{
  "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\nAn impersonation token can never change how the person signs in or recovers their account: changing the\npassword, the sign-in phone, email or User ID, two-factor authentication or backup codes, API tokens, sessions\nor security settings answers `403` with error code `impersonation_cannot_change_account_security`, and the\nperson must make that change from their own sign-in. While impersonation target checks are enabled for the\ninstallation, creating an impersonation token answers `403` with error code `impersonation_target_not_allowed`\nwhen the person may not be impersonated through that token: they are a Super Admin in another organization\nwhere the token's owner isn't one, they also belong to another organization the owner doesn't administer, or\nthey are a Super Admin here and the owner isn't; the error `message` says which. The same checks run on every\nrequest of an open impersonation session, so a token whose person would now be refused stops authenticating\n(`401`). While the checks are disabled, none of these refusals applies.\n\n## Rate Limiting\nEach API token may make 5,000 requests per hour, of which 5,000 may be searches and\n1,000 uploads. Authentication calls are limited to 2,000 per hour for each network\naddress, and requests with no valid token share 10,000 per hour for each network address.\nNarrower limits apply to some costly endpoints.\n\nExceeding a limit answers `429`. Every 429 carries `Retry-After`, the seconds to wait.\nThe platform's limits use error code `rate_limited`; a feature with a limit of its own\ncan use its own code. Rate limit headers are included in responses, for the limit you\nwill reach first:\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\nWait for `Retry-After` before sending the call again. A client that keeps calling while\nrefused can be refused for longer, by the load balancer or by thread time used.\n\n## API Allowance (402)\nEvery call made with an API token someone created (a personal, admin, service-account or CLI\ntoken) counts against the organization's monthly API allowance, then against purchased API\ncredits. Once both are used up, each such call answers `402` with error code\n`api_credits_exhausted` (response `ApiCreditsExhausted`) on every endpoint but `POST /auth/logout`\n(signing out is never refused), until the allowance resets on the 1st, credits are bought, or\nauto top-up buys more (Admin → Billing & Payments → Credits).\nEnterprise plans are not limited. Calls from a signed-in browser session and from the MangoApps\napps' own sign-in are neither counted nor refused, and refused calls (`401`, `402`, `403`, `429`)\nand server errors are not counted.\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## 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-RateLimit-Remaining: 4998\nX-RateLimit-Reset: 1640995200\nX-RateLimit-Limit: 5000\nX-Idempotency-Cached: false\nX-API-Version: 1.0\n```\n\nThese enhancements 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"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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": ""
                          },
                          "hosted_elsewhere": {
                            "type": "boolean",
                            "description": "Present (true) only for a workspace hosted on another\nMangoApps deployment, such as Europe. Its `id` is its id\nthere, `api_base_url` is where to sign in, `logo_url`\nis null and `timezone` is absent. This deployment's own\nworkspaces come first.\n",
                            "example": true
                          },
                          "deployment": {
                            "type": "string",
                            "description": "Present only with `hosted_elsewhere`. The deployment's label, e.g. `eu`.",
                            "example": "eu"
                          }
                        }
                      }
                    },
                    "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"
                    },
                    "hosted_elsewhere": {
                      "type": "boolean",
                      "description": "Present (true) only when the workspace is hosted on another\nMangoApps deployment, such as Europe. `exists` stays false —\nit means \"on this deployment\" — and `api_base_url` is where\nto sign in.\n",
                      "example": true
                    },
                    "api_base_url": {
                      "type": "string",
                      "description": "The origin to sign in at, when the workspace exists here or\nis hosted elsewhere. Absent when it is unknown.\n",
                      "example": "https://acme.onemango.com"
                    }
                  }
                }
              }
            }
          },
          "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/HttpEnhancementMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "description": "account_locked: too many failed sign-in attempts (each wrong password counts); no tokens are issued",
                              "enum": [
                                "two_factor_required",
                                "invalid_otp",
                                "invalid_credentials",
                                "account_locked"
                              ]
                            },
                            "message": {
                              "type": "string"
                            },
                            "details": {
                              "type": "object",
                              "properties": {
                                "requires_2fa": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The credentials were right and no tokens are issued until the person finishes a step on the web.\ntwo_factor_setup_required: the organization requires two-factor authentication of this person\n(everyone, or their role) and they have not set it up, or they started setting it up and never\nconfirmed it. Open details.setup_url (the organization's web sign-in, which leads to setup),\nthen sign in again; the next sign-in asks for otp_code.\npassword_expired: the password is past the organization's expiry period; details.change_url\nis where it is changed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "two_factor_setup_required",
                            "password_expired"
                          ]
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "requires_2fa_setup": {
                              "type": "boolean"
                            },
                            "setup_url": {
                              "type": "string"
                            },
                            "change_url": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/hub_sso": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Sign in with a MangoApps Hub token (mobile apps)",
        "description": "Exchanges a signed MangoApps Hub token for access and refresh tokens, like /auth/login.\nA locked account (too many failed sign-in attempts) is refused with account_locked, and no tokens are issued.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Signed MangoApps Hub token"
                  },
                  "business_id": {
                    "type": "integer",
                    "description": "Organization to sign in to"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in. The same token fields as /auth/login, plus sso_provider."
          },
          "401": {
            "description": "Sign-in refused",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "no_token",
                            "not_configured",
                            "invalid_token",
                            "expired_token",
                            "nonce_reused",
                            "domain_mismatch",
                            "user_deactivated",
                            "jit_disabled",
                            "jit_domain_blocked",
                            "provisioning_failed",
                            "service_account_blocked",
                            "account_locked"
                          ]
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "suggestion": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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"
                  },
                  "code_challenge": {
                    "type": "string",
                    "description": "The app's own PKCE challenge (RFC 7636, RFC 8252 §8.1): the base64url\nSHA-256 (43 characters, no padding) of a random `code_verifier` the app\nkeeps and presents to `/auth/sso/exchange`. The server stores only the\nchallenge, bound to the returned `state`. Optional today; once the\nplatform requires it, a request without one is refused with\n`pkce_required`.\n",
                    "example": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
                  },
                  "code_challenge_method": {
                    "type": "string",
                    "enum": [
                      "S256"
                    ],
                    "description": "Only `S256` is accepted. Defaults to `S256` when a challenge is sent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSO flow initiated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoInitiateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request parameters. `pkce_required` — the platform requires the app's\n`code_challenge` and none was sent. `invalid_pkce` — the challenge is not a\n43-character base64url SHA-256, or the method is not `S256`.\n",
            "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"
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "The PKCE verifier (43-128 characters of `A-Z a-z 0-9 - . _ ~`) whose S256\nchallenge the app sent to `/auth/sso/initiate`. Required whenever the\ninitiate request carried a `code_challenge`; a missing or wrong verifier\nis refused with `invalid_grant` and the state is spent.\n",
                    "example": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSO authentication completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SsoExchangeResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_state` — the state is unknown, expired or already used, or it\nwas issued for another business or another provider (a state is\nbound to the `business_id` AND `provider_id` it was initiated with;\nrestart the flow). `invalid_grant` — the state was issued with a PKCE\n`code_challenge` and the `code_verifier` is missing or does not match\nit. `token_exchange_failed` — the provider refused\nthe code. `missing_parameters` — a required field is blank.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`sso_verification_failed` — the provider's ID token did not verify\n(signature, issuer, audience, expiry or nonce); a failed sign-in,\nnot a missing account. `email_not_verified` — the provider did not\nvouch for the email and no account is linked to this identity yet.\n`user_not_found` — no active account for this person and none was\nprovisioned. `account_locked` — the account is locked after too many\nfailed sign-in attempts; no token is minted. `user_deactivated` —\nthe account cannot sign in.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`user_not_authorized` — the person's membership in this business\nhas ended, or the account may not sign in here.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Business or SSO provider not found (`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. Not the Office add-in's\nrefresh token, which is exchanged at `/auth/office_addin/refresh` for\nthe add-in's own token.\n",
        "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/webview_token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Mint a one-time hand-off into a web session",
        "description": "A native app (mobile or desktop) turns its API sign-in into a browser\nsession: it asks here with its Bearer token, then opens\n`/m/login/native` with the token, which signs the browser in. The\ntoken is signed, works once and expires after 60 seconds.\n\nOnly the access token a sign-in door gave the person, with its full\npermissions, may ask — the browser session it starts has no second\nfactor of its own. Personal, integration and CLI tokens, service\naccounts, impersonation tokens and a browser's own cookie session are\nrefused, and so is a locked or deactivated account.\n\n**Token scopes:** none beyond being the sign-in access token.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The hand-off token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webview_token",
                    "expires_in"
                  ],
                  "properties": {
                    "webview_token": {
                      "type": "string",
                      "description": "Pass it to `/m/login/native` within a minute. It is redeemed once."
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 60
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`authentication_required` or `invalid_token` (no valid Bearer token), or `webview_token_requires_access_token` (the request carried only a browser session cookie).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`webview_token_impersonation_not_allowed`, `webview_token_service_account_not_allowed`, `webview_token_requires_sign_in_token` (a personal, integration or CLI token, or one with narrowed scopes), `webview_token_account_locked` or `webview_token_account_inactive`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                    },
                    "custom_fields": {
                      "type": "array",
                      "description": "The organization's own profile fields on the caller that they may see on themself, as `GET /users/me` lists them. Empty while custom fields are off for the organization.",
                      "items": {
                        "$ref": "#/components/schemas/ProfileCustomField"
                      }
                    },
                    "personnel_file_copy_request": {
                      "type": "object",
                      "nullable": true,
                      "description": "The caller's waiting request for a copy of their personnel file (POST /profile/actions request_personnel_file_copy), or null. Null under View-as.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "example": "requested"
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date"
                        },
                        "due_on": {
                          "type": "string",
                          "format": "date"
                        }
                      }
                    },
                    "personnel_file_copy_sent_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "When HR recorded sending the caller the last copy of their personnel file they asked for (the web and phone \"HR sent you a copy on …\"), or null. Null under View-as."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Authentication"
        ],
        "summary": "Update user profile",
        "description": "Update user profile information including preferences and notification settings.\n\nCalled with an impersonation token, a request whose `phone` (or an `email` or `userid` value) differs\nfrom the stored one answers 403 `impersonation_cannot_change_account_security`; sending the stored\nvalues back unchanged is not refused. When employee self-service profile editing is turned off for the\norganization, a caller below administrator gets 403 `self_service_disabled`.\n",
        "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"
                  },
                  "language": {
                    "type": "string",
                    "nullable": true,
                    "description": "The person's own language, one of the organization's languages. Blank or `null` is \"Not set\"\n— content follows their place's language (Knowledge Layer P4). The value GET /auth/profile\nreturned for someone who never chose one (`language_set: false`) is their place's language;\nposting it back changes nothing. Their own stored value is always accepted back.\n",
                    "example": "fr"
                  },
                  "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"
          },
          "403": {
            "$ref": "#/components/responses/ImpersonationAccountSecurityForbidden"
          },
          "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.\nWhen called with an API token (personal or service account) and `revoke_other_sessions: true`, the account's\nother active API tokens are revoked. A caller authenticated by a web session revokes nothing,\nand `revoked_sessions` reports what actually ran. An impersonation token can't change the\npassword at all: it gets 403 `impersonation_cannot_change_account_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": "Send JSON `true` to revoke the account's other active API tokens. Honoured only for an API token caller (personal or service account)."
                  }
                }
              }
            }
          }
        },
        "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": "True when this call revoked the account's other API tokens (see `revoked_sessions_count`), false when no revocation ran."
                    },
                    "revoked_sessions_count": {
                      "type": "integer",
                      "description": "Number of other API tokens this call revoked; 0 when none were revoked.",
                      "example": 2
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ImpersonationAccountSecurityForbidden"
          },
          "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.\nA successful reset revokes every API token the account holds, in every organization\n(sign-in and refresh tokens, personal and CLI tokens), so each device or integration\nmust sign in again or be issued a new token.\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",
                    "deprecated": true,
                    "description": "Ignored. A successful reset always revokes every API token the account holds."
                  }
                }
              }
            }
          }
        },
        "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": "True on success. Every API token the account held has been revoked.",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "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/connections": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "List my connected assistants",
        "description": "The assistants that can act for the caller — MCP clients they signed in\nto (one row per client), their personal API tokens and their Office\nadd-in sign-ins (one row per Word or Excel pane that signed in) — with\nwhat each may do in plain words, and the caller's latest assistant\nactivity.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's connections",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Connection"
                      }
                    },
                    "recent_activity": {
                      "type": "array",
                      "description": "The caller's latest assistant tool calls, newest first (at most 10).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "client": {
                            "type": "string",
                            "nullable": true,
                            "example": "Claude"
                          },
                          "tool": {
                            "type": "string",
                            "example": "list_workspace_messages"
                          },
                          "outcome": {
                            "type": "string",
                            "enum": [
                              "started",
                              "completed",
                              "failed",
                              "preview",
                              "replayed",
                              "denied"
                            ]
                          },
                          "success": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "connections_enabled": {
                          "type": "boolean",
                          "description": "False when the organization has turned assistant connections off; nothing listed can act until it is turned back on."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          }
        }
      }
    },
    "/auth/connections/{kind}/{id}": {
      "delete": {
        "tags": [
          "Authentication"
        ],
        "summary": "Revoke one of my connected assistants",
        "description": "Disconnects an MCP client (every grant the caller gave it in this\norganization, with its pending authorization codes), revokes a\npersonal API token, or signs out an Office add-in sign-in (its refresh\ntoken and every live add-in access token the caller holds here — the\nadd-in's own sign-out). It stops working at once; a client can be\nconnected again by signing in from it. Needs a token with the\n`write:connections` scope — the app's own sign-in and the CLI carry\nit — so a narrow token cannot revoke the caller's other credentials.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "oauth",
                "token",
                "office_addin"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The client's or token's id, as listed."
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Claude is disconnected"
                    },
                    "revoked": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "oauth",
                            "token",
                            "office_addin"
                          ]
                        },
                        "revoked": {
                          "type": "integer",
                          "description": "Grants or tokens revoked."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "The token has no `write:connections` scope (`insufficient_permissions`),\nor it is an impersonation session, which cannot revoke\n(`impersonation_cannot_change_account_security`).\n"
          },
          "404": {
            "description": "Not one of the caller's connections"
          }
        }
      }
    },
    "/auth/office_addin/device": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Start the Office add-in's sign-in",
        "description": "Issues a device code (kept by the add-in) and a short user code the\nperson checks on the sign-in page, bound to the workspace whose\naddress the request was sent to. Open `verification_url_complete` in\nthe person's browser (or show `verification_url` and the code to type\non any device), then poll `/auth/office_addin/token` every `interval`\nseconds until `expires_in` runs out.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "A code to allow",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "device_code": {
                      "type": "string",
                      "description": "Secret. Sent only in the body of `/auth/office_addin/token`."
                    },
                    "user_code": {
                      "type": "string",
                      "example": "ABCD-EFGH"
                    },
                    "verification_url": {
                      "type": "string",
                      "example": "https://acme.workforce.mangoapps.com/auth/office-addin"
                    },
                    "verification_url_complete": {
                      "type": "string",
                      "example": "https://acme.workforce.mangoapps.com/auth/office-addin?code=ABCD-EFGH"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 600
                    },
                    "interval": {
                      "type": "integer",
                      "example": 5
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`workspace_required` — the address names no MangoApps workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many sign-ins started from this address."
          }
        }
      }
    },
    "/auth/office_addin/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Collect the Office add-in's sign-in",
        "description": "Poll with the device code until the person allows or denies the\nsign-in. Once allowed, the first poll returns a one-hour access token\nand a 30-day refresh token; every later poll of the same code answers\n410. The tokens are minted then — never stored in plaintext waiting\nfor a poll — and the response is not cacheable.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "device_code"
                ],
                "properties": {
                  "device_code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "approved"
                    },
                    "access_token": {
                      "type": "string"
                    },
                    "refresh_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 3600
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "read:contracts",
                        "write:contracts",
                        "destructive:contracts",
                        "read:drive",
                        "write:drive"
                      ]
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "business": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "subdomain": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Not allowed yet — poll again after `interval` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "authorization_pending"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — no device_code."
          },
          "403": {
            "description": "`status: denied` — the person chose Deny, or their account may no\nlonger sign in (`error.code: access_denied`).\n"
          },
          "404": {
            "description": "`invalid_device_code` — not a code this door issued."
          },
          "410": {
            "description": "`status: expired` — the code expired or was already collected (`error.code: expired_token`)."
          },
          "429": {
            "description": "Polled too often from this address; poll more slowly."
          }
        }
      }
    },
    "/auth/office_addin/refresh": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Refresh the Office add-in's access token",
        "description": "A new one-hour access token for the add-in's live refresh token, while\nits person may still sign in. Only the add-in's refresh token is\naccepted here, and the app sign-in's `/auth/refresh` does not accept\nit.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refresh_token"
                ],
                "properties": {
                  "refresh_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A new access token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 3600
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_refresh_token` — the sign-in has ended; sign in again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/office_addin/sign_out": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Sign the Office add-in out",
        "description": "Revokes the add-in's own access token (the Bearer header) and refresh\ntoken (the body) — either is enough, so a pane whose access token ran\nout can still sign out. Any other token is left alone: a token pasted\ninto the add-in is never revoked here.\n",
        "security": [],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "example": "Bearer <the add-in access token>"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "refresh_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "signed_out": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_token` — neither credential is an Office add-in sign-in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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. Needs a token\nwith the `write:connections` scope.\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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/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. Needs a token with the\n`write:connections` scope.\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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          },
          "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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/auth/2fa/backup_codes": {
      "get": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Get backup codes (retired verb)",
        "deprecated": true,
        "description": "Retired: a GET carried the account password in the URL, where proxy and load-balancer\nlogs, browser history and Referer headers keep it. Answers 405 naming the POST below\n(or 400 `password_in_url` when the URL carries `current_password`).\n",
        "responses": {
          "400": {
            "description": "`password_in_url` — the URL carried `current_password`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed` — read backup codes with POST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Two-Factor Authentication"
        ],
        "summary": "Get backup codes",
        "description": "Retrieve current backup codes for 2FA recovery. Requires the current password in the\nrequest body (a `current_password` in the URL is refused with `password_in_url`) and a\ntoken carrying `write:connections`. Each read is recorded on the person's activity log.\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 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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          }
        }
      }
    },
    "/auth/passwordless/request": {
      "post": {
        "tags": [
          "Passwordless Authentication"
        ],
        "summary": "Request passwordless authentication (unavailable)",
        "deprecated": true,
        "description": "Not available. This endpoint used to accept a request for a magic link, email code or\nSMS code but never delivered one, so no request could end in a sign-in. It now issues\nnothing and answers every request, whatever its body, with the same\n`403 feature_disabled`. `GET /auth/login_config` reports `login_methods.passwordless.enabled: false`.\nSign in with `POST /auth/login` or single sign-on.\n\n`POST /auth/passwordless/verify` is unaffected: it still redeems sign-in links issued elsewhere.\n",
        "security": [],
        "responses": {
          "403": {
            "description": "Passwordless sign-in is not available. The same body is returned for every request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "feature_disabled",
                    "message": "Sign-in links and codes are not available through the app. Sign in with your password or single sign-on."
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests for this address or from this IP",
            "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.\nAccepts magic-link tokens only, and each token signs in once. Six-digit\nemail and SMS codes belong to the web and phone sign-in doors that sent\nthem and are answered here as an unknown token (401 invalid_token).\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",
                      "description": "Whether `POST /auth/passwordless/request` issues anything. Always false.",
                      "example": false
                    },
                    "recent_tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PasswordlessToken"
                      }
                    },
                    "supported_types": {
                      "type": "array",
                      "description": "Types `POST /auth/passwordless/request` issues. Always empty.",
                      "items": {
                        "type": "string"
                      },
                      "example": []
                    },
                    "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": "Number of your personal API tokens, the same set `data` lists (no service accounts, no tokens the app mints at sign-in); revoked and expired tokens included"
                        },
                        "active_tokens": {
                          "type": "integer",
                          "description": "How many of those personal tokens are active (not revoked, not expired)"
                        },
                        "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, and a token with\nthe `write:connections` scope (the app's own sign-in and the CLI carry\nit; a token you make carries it only when you grant it).\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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          },
          "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, and a\ntoken with the `write:connections` scope.\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 revoked."
                    },
                    "deleted_token": {
                      "$ref": "#/components/schemas/PersonalApiToken"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          },
          "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, and a\ntoken with the `write:connections` scope.\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"
          },
          "403": {
            "$ref": "#/components/responses/CredentialsWriteForbidden"
          },
          "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. The token value is returned only in this response; it is stored as a digest and cannot be shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "description": "Impersonation token (use as a Bearer token in the Authorization header)",
                      "example": "a1b2c3d4e5f6..."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Token expiration time",
                      "example": "2024-01-15T14:30:00Z"
                    },
                    "impersonated_user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "email": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "instructions": {
                      "type": "object",
                      "properties": {
                        "usage": {
                          "type": "string"
                        },
                        "example": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is not a service account (`service_account_required`), or the service account lacks the `impersonate` or `admin` scope (`insufficient_scope`).\n\n`impersonation_target_not_allowed` (only while impersonation target checks are enabled for the installation): the person may not be impersonated through this token. The `message` gives the reason:\n- \"This person is a Super Admin in another organization, so only a Super Admin there can view the system as them.\"\n- \"This person also belongs to another organization you don't administer, so only someone who administers every other organization they belong to can view the system as them.\" (\"you\" is the token's owner.)\n- \"Only a token whose owner is a Super Admin in this organization can impersonate a Super Admin. Re-issue the token under a person who is a Super Admin here; a service account can't be one.\"\n\nWhile the checks are enabled they also run on every request of an open impersonation session, and a token whose person would now be refused gets `401`. While they are disabled, none of these refusals applies.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "insufficient_scope",
                    "message": "Service account requires 'impersonate' or 'admin' scope"
                  }
                }
              }
            }
          },
          "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 active impersonation tokens",
        "description": "Returns the active impersonation tokens this service account created in the current\nbusiness, newest first, at most 100. Requires a service account token with the\n`impersonate` or `admin` scope; any other caller, including an impersonation token,\nis refused with 403.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Impersonation tokens retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "Impersonation token record ID. The token value itself is never listed; it is returned once, when the token is created."
                          },
                          "impersonated_user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "email": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Token expiration time"
                          },
                          "revoked": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Number of tokens returned (at most 100)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller is not a service account (`service_account_required`), or the service account lacks the `impersonate` or `admin` scope (`insufficient_scope`).",
            "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\n**Required Scopes:** `read:widgets` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\") — the same aggregated, person-scoped\nhome payload as `GET /api/v1/widgets`, which asks it. A token without it\ngets `403 insufficient_permissions` (\"Required scope: read:widgets\").\nThe Inbox nodes inside (`waiting_on_you`, `notifications`, a manager's\n`quick_stats.pending_approvals`) still ask `read:notifications` on top,\nand are `null` without it. The sign-in tokens hold both; a browser\nsession is asked no scope.\n",
        "parameters": [
          {
            "name": "waiting_on_you_older",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Opt in to `waiting_on_you.older` — what is still waiting on the caller but was asked more than 90 days ago (the web Inbox's collapsed \"Older\" group). Absent unless `1`/`true`; it costs a second pass over the approvals queues.\n"
          },
          {
            "name": "surface",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "desktop"
              ]
            },
            "description": "`desktop` answers one section of the desktop app's Home instead of the payload below; `section` says which.\n"
          },
          {
            "name": "section",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "manifest",
                "attention",
                "focus",
                "schedule",
                "today",
                "you",
                "review",
                "week"
              ]
            },
            "description": "With `surface=desktop`. `attention` is Waiting on you with what each row asks (`read:notifications` on top). `focus` is the desktop's \"Do this first\": up to three of the person's ledger rows, most urgent first, leaving out what Waiting on you lists. Each row's `can_snooze` and `can_dismiss` say what this token may do. `schedule` is the desktop Today's schedule: today's calendar in the person's zone with each meeting's join link, their answer and its AI Notepad note, the first invites waiting for an answer, and tomorrow's first item once the day is clear, and for a manager `team_out`: who on their team is out today, with the reason only where the viewer may see it. It asks the Calendar's `read:calendar_sync` and leaves out acknowledgment deadlines, which Waiting on you lists. `you` is the right column: the person's time off (balances, a request waiting, the next approved leave and the leave types they may request), recognition received this week, today's birthdays and work anniversaries in their department, and for a manager one report to recognize. `review` is To review: up to three items the person owns whose \"Still accurate?\" review is overdue or due soon, soonest first, as Content I own lists them, with `total` and each item's `can_mark`. `week` is the weekly wrap: the signed-in person's own week in at most five lines, counted by rules, from Friday 12:00 to Monday 12:00 in their zone. It answers `state` (`shown`, `outside_window`, `off_for_company`, `off_for_you`, or `unavailable` for an impersonated session, which is never answered any lines), `company_enabled` and `personal_enabled` (always present), `week_of` (the Monday of the week counted) and `shows_until` (the next Monday 12:00), both null unless shown, and `lines`: the first five, in this order, of `recognized_by` {count, names}, `messages` {sent, received, conversations, top: [{type: person|room, name}]}, `post_acknowledged` {room_name, acknowledged, of}, `clip_watched` {title, views, viewers}, `meetings` {meetings, noted, action_items}, `first_conversations` {count}, `mentions` {count} and `reactions` {received}, each sent only when it has something to say. The week counted is Monday 00:00 to Friday 12:00, worked out once and kept until the wrap closes; it counts facts, never scores or comparisons. There is no parameter to ask for anyone else's week.\n"
          },
          {
            "name": "calendars",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "connected"
              ]
            },
            "description": "With `section=schedule`: `connected` answers the connected Google / Outlook calendars alone. Those may be read live from the provider, so a client asks for them apart and the rest never waits on them.\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": {
                            "language": {
                              "type": "string",
                              "description": "The language content reaches the caller in — own\nsetting, else their place's, else the company\ndefault. Never null (Knowledge Layer P4).\n",
                              "example": "en"
                            },
                            "language_set": {
                              "type": "boolean",
                              "description": "Whether the caller chose a language themselves (`false` is \"Not set\")."
                            },
                            "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": {
                        "punches_paused": {
                          "type": "boolean",
                          "description": "True while the tenant's maintenance refuses punches for this caller (a read-only freeze, or a window that blocks every worker door): draw Clock In / Clock Out, breaks and undo disabled instead of offering a punch the server answers with a 503.\n"
                        },
                        "paused_until": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the pause is expected to end (ISO 8601); null when it lifts only when an administrator lifts it, or when nothing is paused."
                        },
                        "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"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "your_tasks": {
                      "type": "object",
                      "nullable": true,
                      "description": "The Home briefing's \"Your tasks\": what the caller owes across apps — tasks, courses, onboarding items, documents to sign, inspections, shift offers, Frontline work — ranked overdue first. Null unless an administrator turned the Home briefing on. Must-reads and approvals are in `waiting_on_you`.\n",
                      "properties": {
                        "items": {
                          "type": "array",
                          "description": "The first five rows.",
                          "items": {
                            "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",
                                  "onboarding",
                                  "signatures"
                                ],
                                "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"
                              },
                              "mobile_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "The row's phone page, where it is not simply the phone form of `url` (an onboarding form). Null otherwise."
                              },
                              "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."
                              },
                              "blocked": {
                                "type": "boolean",
                                "description": "Whether the worker has flagged this as \"I can't do this\". Only a `frontline` row can be flagged; every other source is always false.",
                                "example": true
                              },
                              "blocked_reason": {
                                "type": "string",
                                "nullable": true,
                                "description": "What is blocking it — the reason the worker picked when flagging. Null when not flagged.",
                                "enum": [
                                  "no_stock",
                                  "missing_fixture_or_supplies",
                                  "equipment_down",
                                  "not_enough_time",
                                  "unclear_instructions",
                                  "other"
                                ],
                                "example": "missing_fixture_or_supplies"
                              },
                              "blocked_reason_label": {
                                "type": "string",
                                "nullable": true,
                                "description": "Human-readable label for `blocked_reason`. Null when not flagged.",
                                "example": "Missing fixture or supplies"
                              },
                              "blocked_note": {
                                "type": "string",
                                "nullable": true,
                                "maxLength": 500,
                                "description": "The note the worker left for their manager when flagging it (\"Anything your manager should know\"). Optional when flagging, so it can be null on a flagged row; always null on an unflagged one.",
                                "example": "Promo signage never arrived in the delivery"
                              },
                              "blocked_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "When it was flagged. Null when not flagged."
                              }
                            }
                          }
                        },
                        "total": {
                          "type": "integer",
                          "description": "How many rows the whole list holds."
                        },
                        "degraded_sources": {
                          "type": "array",
                          "description": "Apps whose rows could not be loaded, so the list is incomplete.",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "trip": {
                      "type": "object",
                      "nullable": true,
                      "description": "The Home briefing's \"Your assignment\": the dated work assignment (a posting, a rotation, a trip) the caller is on or about to start — the one under way, else the next. Null when the caller has none, and unless an administrator turned the Home briefing on.\n",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "location": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "nullable": true
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "starts_on": {
                          "type": "string",
                          "format": "date"
                        },
                        "ends_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "Null while the assignment has no end date."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "active"
                          ]
                        },
                        "countdown": {
                          "type": "string",
                          "nullable": true,
                          "description": "\"Starts in 9 days\", \"Starts tomorrow\", \"Day 3 of 28\"."
                        },
                        "report_to": {
                          "type": "string",
                          "nullable": true,
                          "description": "Full name of the person to report to on arrival."
                        },
                        "report_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When to report, in the time zone of the location."
                        },
                        "travel": {
                          "type": "array",
                          "description": "The legs of the journey there, in order.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "mode": {
                                "type": "string",
                                "enum": [
                                  "flight",
                                  "helicopter",
                                  "ferry",
                                  "train",
                                  "bus",
                                  "car",
                                  "other"
                                ]
                              },
                              "label": {
                                "type": "string",
                                "description": "\"Flight · Aberdeen → Bergen\"."
                              },
                              "detail": {
                                "type": "string",
                                "nullable": true,
                                "description": "The departure time as the ticket shows it, and the reference."
                              }
                            }
                          }
                        },
                        "before_you_go_count": {
                          "type": "integer",
                          "description": "Open pre-start items on the pre-arrival plan opened for this assignment; 0 with no plan."
                        }
                      }
                    },
                    "waiting_on_you": {
                      "type": "object",
                      "nullable": true,
                      "description": "The ONE \"Waiting on you\" (Knowledge Layer E2) — the same list the Home briefing, the rail's per-app counts and the Inbox's ?view=waiting (and /m/inbox?view=waiting) show, over what was asked in the last 90 days (older asks: `older`). Two halves: what the caller DECIDES (`to_decide` — every approvals queue whose decision is theirs) and what they were asked to read and confirm and have not (`to_acknowledge_count`, `items`, `counts_by_kind` — must-read posts, broadcasts, alerts, Sites pages, policies and SOPs, each from that app's own pending list, never from notification rows, so an ask in a muted category is still here). Not summed into `quick_stats.unread_notifications`. `null` if the computation fails, and for a token without `read:notifications` — as are `notifications` and, for a manager, `quick_stats.pending_approvals` (the Inbox is read only with that scope).\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "description": "Everything asked in the last 90 days, uncapped — `to_decide.count` plus `to_acknowledge_count`.\n"
                        },
                        "to_decide": {
                          "type": "object",
                          "description": "\"To decide\" — what the caller decides (the approvals registry: people's requests, AI suggestions, …). The same number as `quick_stats.pending_approvals`.\n",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "Everything to decide asked in the last 90 days, uncapped"
                            },
                            "counts_by_source": {
                              "type": "object",
                              "description": "The count split by approvals source (e.g. `leave_requests`)",
                              "additionalProperties": {
                                "type": "integer"
                              }
                            },
                            "items": {
                              "type": "array",
                              "maxItems": 10,
                              "description": "A preview, most urgent first (overdue, then soon, then oldest request), capped at 10. Decided where `url` links.\n",
                              "items": {
                                "$ref": "#/components/schemas/WaitingOnYouDecideRow"
                              }
                            }
                          }
                        },
                        "to_acknowledge_count": {
                          "type": "integer",
                          "description": "\"To acknowledge\" — everything the caller was asked in the last 90 days to confirm and has not, uncapped (`items` / `counts_by_kind`).\n"
                        },
                        "counts_by_kind": {
                          "type": "object",
                          "description": "The \"To acknowledge\" count split by kind (kinds with nothing waiting are omitted)",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "items": {
                          "type": "array",
                          "maxItems": 10,
                          "description": "\"To acknowledge\" preview, most urgent first (overdue, then soonest due, then newest ask), capped at 10.\n",
                          "items": {
                            "$ref": "#/components/schemas/WaitingOnYouAcknowledgeItem"
                          }
                        },
                        "degraded": {
                          "type": "array",
                          "description": "What failed to load just now — acknowledgement kinds and approvals sources — so the list may be missing them\n",
                          "items": {
                            "type": "string"
                          }
                        },
                        "older": {
                          "type": "object",
                          "description": "Only with `?waiting_on_you_older=1` (the key is absent otherwise). Still open but asked more than 90 days ago, so in no count above and no badge — the web Inbox's collapsed \"Older\" group.\n",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "Everything older, uncapped (`to_decide_count` + `to_acknowledge_count`)"
                            },
                            "to_decide_count": {
                              "type": "integer"
                            },
                            "to_acknowledge_count": {
                              "type": "integer"
                            },
                            "to_decide": {
                              "type": "array",
                              "maxItems": 20,
                              "description": "Decided with `POST /home/waiting_on_you/decide`, as `to_decide.items`",
                              "items": {
                                "$ref": "#/components/schemas/WaitingOnYouDecideRow"
                              }
                            },
                            "to_acknowledge": {
                              "type": "array",
                              "maxItems": 20,
                              "description": "Same item shape as `waiting_on_you.items`",
                              "items": {
                                "$ref": "#/components/schemas/WaitingOnYouAcknowledgeItem"
                              }
                            },
                            "degraded": {
                              "type": "array",
                              "items": {
                                "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
                              },
                              "link_withheld_app": {
                                "type": "string",
                                "nullable": true,
                                "description": "When `action_url` is null because the app it leads into will not let this reader in, that app's name; null otherwise.\n"
                              }
                            }
                          }
                        }
                      }
                    },
                    "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": "What the caller decides — the approvals registry's count, the same number as `waiting_on_you.to_decide.count` and the web home and approvals page (managers/admins only). Was a business-wide count of pending leave requests and attendance records flagged for review, whoever could act.\n"
                        },
                        "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/HttpEnhancementMeta"
                        },
                        {
                          "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"
          },
          "403": {
            "description": "The token lacks `read:widgets` (`insufficient_permissions`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/home/waiting_on_you/decide": {
      "post": {
        "tags": [
          "Home"
        ],
        "summary": "Decide a \"To decide\" row of Waiting on you",
        "description": "Approve or decline one `waiting_on_you.to_decide.items` row from\n`GET /api/v1/home` (Knowledge Layer C3 — approve anywhere). `source` and\n`id` are the row's own. The approvals registry's one decide path — the\nsame one the web and phone \"Waiting on you\", the approvals page and the\nTeams / Slack cards take: the queue's door re-checked now, the row in\nthe caller's own queue right now, a reason where the queue asks one of\na decline, then the owning app's own action, run as the caller.\n\"Not waiting\" is one answer for a row already decided and a row that\nwas never the caller's, so it reveals nothing about someone else's.\n\nA token needs the `write:approvals` scope (the native app's sign-in\ntokens carry it); without it the row's `can_approve` / `can_decline`\nread false and this answers 403 `insufficient_permissions`.\n\nDelete-level (owner decisions 2026-10-04, #6659 and #6721): EVERY decline\n— a decision that ends someone else's request — also requires the delete\nscope of the area the request belongs to: declining a leave request\n`destructive:leave_management`, a timesheet `destructive:timekeeping`, an\nexpense report `destructive:mango_spend`, a form submission\n`destructive:forms`, a goal or review `destructive:epms`, a contract\n`destructive:contracts`, an offer `destructive:offer_manager`, and so on\n(an AI suggestion is the one exception: declining it ends nobody's\nrequest). Approving a nomination asks `destructive:recognition` too.\nWithout it the row's `can_approve` / `can_decline` read false for that\nverdict and this answers 403 `insufficient_permissions` naming the scope.\n",
        "operationId": "decideWaitingOnYou",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source",
                  "id",
                  "verdict"
                ],
                "properties": {
                  "source": {
                    "type": "string",
                    "description": "The row's approvals source key (`to_decide.items[].source`).",
                    "example": "ai_suggestions"
                  },
                  "id": {
                    "type": "integer",
                    "description": "The row's id (`to_decide.items[].id`)."
                  },
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "reject"
                    ]
                  },
                  "surface": {
                    "type": "string",
                    "enum": [
                      "inbox",
                      "approvals_page",
                      "home",
                      "today",
                      "phone",
                      "email",
                      "teams",
                      "slack",
                      "ask_ai",
                      "chat"
                    ],
                    "description": "The screen the person decides on, when the client knows it. It is what a Chat card says the decision came from; any other value is ignored.\n"
                  },
                  "reason": {
                    "type": "string",
                    "description": "Required to decline a row whose `decline_needs_reason` is true."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "The owning app's own sentence."
                    },
                    "waiting_on_you": {
                      "type": "object",
                      "nullable": true,
                      "description": "The counts after the decision, so the Home badge moves with it; `null` for a token without `read:notifications`.\n",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "to_decide_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token has no `write:approvals` scope, or no delete scope for a delete-level verdict (the message names it); `queue_unavailable` — no such queue, or its app is off for this workspace.\n"
          },
          "409": {
            "description": "`not_waiting` — already decided, or not the caller's to decide."
          },
          "422": {
            "description": "`reason_required` (decline with no reason where the queue asks one), `invalid_verdict`, or `decision_refused` (the owning app's own refusal, in `error.message`).\n"
          }
        }
      }
    },
    "/home/focus/{id}/snooze": {
      "post": {
        "tags": [
          "Home"
        ],
        "summary": "Snooze a \"Do this first\" row",
        "description": "Parks one row of `GET /api/v1/home?surface=desktop&section=focus` in the\ncaller's own feed, and it comes back when the time is up. The same writer\nas AI Home's Snooze: it needs the row to be in the caller's feed, not\nauthority over it, and it changes nothing for anyone else.\n\nA token needs the `write:approvals` scope; without it the row's\n`can_snooze` reads false and this answers 403 `insufficient_permissions`.\n",
        "operationId": "snoozeHomeFocusRow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 168,
                    "default": 4,
                    "description": "How long, in hours. Kept within 1 hour and 7 days."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Snoozed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token has no `write:approvals` scope."
          },
          "404": {
            "description": "`not_found` — the row is not in the caller's feed."
          }
        }
      }
    },
    "/home/focus/{id}/dismiss": {
      "post": {
        "tags": [
          "Home"
        ],
        "summary": "Dismiss a \"Do this first\" row",
        "description": "Closes one row of `GET /api/v1/home?surface=desktop&section=focus`, on AI\nHome's Dismiss rules: never a report's own obligation, and only for\nsomeone who decides that row. A row with a decision in it is answered\nfrom \"To decide\" instead, with `POST /home/waiting_on_you/decide`.\n\nA token needs the `write:approvals` scope; without it the row's\n`can_dismiss` reads false and this answers 403 `insufficient_permissions`.\n",
        "operationId": "dismissHomeFocusRow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why, kept on the row's record."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token has no `write:approvals` scope; `forbidden` — the row is not the caller's to close.\n"
          },
          "404": {
            "description": "`not_found` — the row is not in the caller's feed."
          },
          "422": {
            "description": "`decide_instead` — the row has a decision in it; or `unprocessable_entity` — it was already decided, or its app needs a reason given through Decline.\n"
          }
        }
      }
    },
    "/home/review/{subject_type}/{id}/still_accurate": {
      "post": {
        "tags": [
          "Home"
        ],
        "summary": "Mark an item from To review still accurate",
        "description": "Marks one item of `GET /api/v1/home?surface=desktop&section=review` as\nstill accurate, as Content I own's Still accurate does: its review clock\nrestarts without an edit, and its stale finding resolves. Only the\nperson the item is listed for (its owner, a member of its owning team,\nor its author while nobody is named) who can open it may do it.\n\nA token needs the `write:widgets` scope; without it this answers 403\n`insufficient_permissions`.\n",
        "operationId": "markHomeReviewStillAccurate",
        "parameters": [
          {
            "name": "subject_type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The item's `subject_type` from the review row, such as `Sop`."
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Marked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "next_review_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "When its review falls due again; null while governance is off for its app."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token has no `write:widgets` scope; `forbidden` — the item is not listed for the caller.\n"
          },
          "404": {
            "description": "`not_found` — no item of a type this can mark has that id."
          }
        }
      }
    },
    "/home/week/preference": {
      "patch": {
        "tags": [
          "Home"
        ],
        "summary": "Turn the weekly wrap on Today off or on for yourself",
        "description": "The signed-in person's own switch for the weekly wrap\n(`GET /api/v1/home?surface=desktop&section=week`), in this company.\nOff, the wrap answers `off_for_you` and is never worked out for them.\nThere is no parameter for anyone else, and an impersonated session may\nnot change it.\n\nA token needs the `write:widgets` scope; without it this answers 403\n`insufficient_permissions`.\n",
        "operationId": "updateHomeWeekPreference",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "false turns the wrap off for the caller; true turns it back on."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "personal_enabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token has no `write:widgets` scope; `impersonated` — the session is acting as someone else.\n"
          },
          "422": {
            "description": "`invalid_parameter` — `enabled` is not true or false."
          }
        }
      }
    },
    "/users/me": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get current user profile (Enhanced)",
        "description": "Returns the authenticated user's profile and business information, with optional\n`include` fields 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": {
                      "$ref": "#/components/schemas/CurrentUser"
                    },
                    "business": {
                      "$ref": "#/components/schemas/Business"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/HttpEnhancementMeta"
                    }
                  }
                }
              }
            }
          },
          "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}": {
      "get": {
        "tags": [
          "Users"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a user's HRIS detail (by employee UID)",
        "description": "The HRIS detail of one person in the caller's business, looked up by\n`mango_employee_id` (the `uid` the list returns — e.g. `E1234567`), not\nthe numeric id (that is `GET /users/{id}/profile`). Requires\n`read:people_directory` (`read:users` implies it).\n\n`legal_entity` (Knowledge Layer X5) is the person's employing legal\nentity: their own override, else the nearest one set on their location\nor a place above it; null when neither names one. `language` is their\nstored SETTING (null = \"Not set\", they follow their place) — a profile\nfield, unlike `/users/me`, whose `language` is the resolved one.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The person's employee UID (`mango_employee_id`).",
            "schema": {
              "type": "string",
              "example": "E1234567"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/UserDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `read:people_directory`."
          },
          "404": {
            "description": "`user_not_found` — no one in this business has that UID."
          }
        }
      }
    },
    "/users/by_email/{email}": {
      "get": {
        "tags": [
          "Users"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a user's HRIS detail (by email)",
        "description": "The same payload as `GET /users/{id}`, looked up by email address\n(case-insensitive) among the caller's business's members. Requires\n`read:people_directory`.\n",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/UserDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `read:people_directory`."
          },
          "404": {
            "description": "`user_not_found`."
          }
        }
      }
    },
    "/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
                                  }
                                }
                              }
                            }
                          }
                        },
                        "custom_fields": {
                          "type": "array",
                          "description": "The organization's own profile fields on this person that the caller may see there, as `GET /users/{id}` lists them.",
                          "items": {
                            "$ref": "#/components/schemas/ProfileCustomField"
                          }
                        },
                        "can_message": {
                          "type": "boolean",
                          "description": "Whether the caller may start a message with this person, as the web profile's Message button decides: Messages is theirs to start, no information barrier stands between the two, and the token may write messages (`write:messages`). False on your own profile."
                        },
                        "message_restriction": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the caller may not message this person when an information barrier is the reason, in the caller's language (the sentence the web profile shows where the Message button would be). The barrier covers Chat too, so a client hides its Message and Chat buttons and shows this instead; it is set whenever Messages or Chat is open to the caller, so a company with Chat and without Messages still gets it. Null otherwise."
                        },
                        "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.\nThe caller's answers to the organization's own profile fields are in\n`user.custom_fields` there.\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",
                      "set_emergency_contact_detention_notice",
                      "request_manager_change",
                      "update_communication_preferences",
                      "request_personnel_file_copy"
                    ],
                    "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",
                      "set_emergency_contact_detention_notice",
                      "request_manager_change",
                      "update_communication_preferences",
                      "request_personnel_file_copy"
                    ],
                    "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"
                      }
                    }
                  }
                },
                "set_emergency_contact_detention_notice": {
                  "summary": "Say whether your emergency contact is told if you are arrested or detained at work (California SB 294)",
                  "value": {
                    "action_type": "set_emergency_contact_detention_notice",
                    "data": {
                      "answer": "yes"
                    }
                  }
                },
                "request_personnel_file_copy": {
                  "summary": "Ask HR for a free copy of your own personnel file (no data; never under View-as — 403 impersonation_not_allowed; 409 already_requested while one is waiting, details.request is that one; on success data.request is { id, status, requested_at, due_on })",
                  "value": {
                    "action_type": "request_personnel_file_copy"
                  }
                },
                "request_manager_change": {
                  "summary": "Ask for a change of manager",
                  "description": "Files a People profile change request for an approver. `reason` is required. `effective_date`\n(`YYYY-MM-DD`, today or later) is optional, unless the People approval workflow the change meets\nhas \"Require Effective Date\" on: then a filing without one answers 422 `effective_date_required`.\nA date that is not a `YYYY-MM-DD` string answers 400 `invalid_effective_date`, and a past date 422\n`manager_request_invalid`. The filed request is returned with its `effective_date`.\n",
                  "value": {
                    "action_type": "request_manager_change",
                    "data": {
                      "manager_id": 42,
                      "reason": "I moved to the evening team",
                      "effective_date": "2026-11-02"
                    }
                  }
                }
              }
            }
          }
        },
        "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"
          },
          "403": {
            "description": "`self_service_disabled` (a profile edit while employee self-service is off), or `impersonation_not_allowed` (request_personnel_file_copy under View-as).\n"
          },
          "409": {
            "description": "`already_requested` (request_personnel_file_copy while one is waiting; `error.details.request` is that one: { id, status, requested_at, due_on }).\n"
          },
          "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\nCalled with an impersonation token, a request whose `personal` section carries a `phone`, `email` or\n`userid` value that differs from the stored one answers 403 `impersonation_cannot_change_account_security`\nand saves nothing; sending the stored values back unchanged is not refused.\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"
                              ]
                            }
                          }
                        }
                      },
                      "custom_fields": {
                        "type": "object",
                        "description": "The caller's own answers to the organization's custom profile fields, as\n`{ field_name: answer }`: a string, a list of strings for a multi-select, and\ntrue/false (or yes/no) for a yes-or-no field. Only fields the caller may answer\nthemselves are accepted; a field that needs approval is refused with where to ask\nfor it. Any refusal saves nothing in the whole update.\n",
                        "additionalProperties": {
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "boolean"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "example": {
                          "tshirt_size": "L",
                          "languages_spoken": [
                            "English",
                            "French"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "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. `custom_fields` is `{ success, updated }`, the\nfield names saved.\n"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ImpersonationAccountSecurityForbidden"
          },
          "422": {
            "description": "A section refused what was sent (`validation_failed`): `error.message` says why, and\n`error.details.errors` lists one sentence per refusal. Nothing in the update was saved.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          },
          "403": {
            "$ref": "#/components/responses/ImpersonationAccountSecurityForbidden"
          },
          "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\n**Required Scopes:** `read:own_profile` — the person's own first-run\nsetup (#7183, owner rule 2026-10-06 \"every api/v1 door asks a scope\").\nA token without it gets `403 insufficient_permissions` (\"Required scope:\nread:own_profile\") before anything else is checked. The sign-in tokens\nhold it; a browser session is asked no scope.\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",
                      "nullable": true,
                      "description": "The first step that applies to this person and is not yet done. `null` once `completed` is true.",
                      "example": "availability_shift_preferences"
                    },
                    "progress_percentage": {
                      "type": "integer",
                      "description": "Completion percentage (0-100), over the steps that apply to this person",
                      "example": 50
                    },
                    "steps_completed": {
                      "type": "integer",
                      "description": "Number of applicable steps completed",
                      "example": 2
                    },
                    "steps_total": {
                      "type": "integer",
                      "description": "Number of steps that apply to this person (the ones `GET /onboarding/steps` lists) — steps skipped for the company or the person are not counted",
                      "example": 4
                    },
                    "estimated_completion_time": {
                      "type": "string",
                      "description": "Estimated time to complete remaining steps (3 minutes a step)",
                      "example": "6 minutes"
                    },
                    "next_action": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL for the next step to complete; `null` once completed",
                      "example": "/api/v1/onboarding/steps/availability_shift_preferences"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `read:own_profile` (`insufficient_permissions`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\nOnly the steps that apply to this person are listed: a step the company\ndoes not use, and `security_setup` for a person who signs in through\ntheir organization, are left out.\n\n**Required Scopes:** `read:own_profile` — the person's own first-run\nsetup (#7183, owner rule 2026-10-06 \"every api/v1 door asks a scope\").\nA token without it gets `403 insufficient_permissions` (\"Required scope:\nread:own_profile\") before anything else is checked. The sign-in tokens\nhold it; a browser session is asked no scope.\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 including name and contact details."
                          },
                          "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 listed",
                      "example": 4
                    },
                    "completed_count": {
                      "type": "integer",
                      "description": "Number of completed steps",
                      "example": 2
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `read:own_profile` (`insufficient_permissions`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\n**What each step's `form_fields` publish:**\n- `profile_completion` — `first_name`, `last_name`, `phone`, `time_zone`\n  (editable); `email` and `userid` (read-only, each only when the person\n  has one); `job_title` (**read-only**: \"Your job title is managed by\n  your administrator.\"). There is no profile photo field — the step\n  saves none.\n- `availability_shift_preferences` — `min_hours_weekly` and\n  `max_hours_weekly` (`min: 0`, `max: 80`), `max_consecutive_days`\n  (`min: 1`, `max: 7`), `weekend_availability` (`all`, `alternating`,\n  `occasional`, `none`), `holiday_availability` (`all`, `some`, `none`)\n  and `willing_for_extra_shifts`.\n- `notification_preferences` — `email_notifications` and\n  `sms_notifications` only; either is `read_only: true` with the reason\n  in `help_text` when that channel cannot reach the person. Shift\n  reminder timing and quiet hours are no longer published as fields\n  (`current_data` still carries `shift_reminder_hours`,\n  `quiet_hours_start` and `quiet_hours_end` for apps that read them).\n- `security_setup` — `password`, `password_confirmation`, and\n  `current_password` unless it is waived for this caller.\n\n**A skipped step** answers 200 with `status: \"skipped\"`, `skipped: true`,\n`skipped_reason`, `accessible: false`, `required: false` and `message`\n(\"This step has been skipped: <reason>.\") — and none of the form keys.\nThe reason is one of: \"Not part of this company's onboarding\"\n(`role_location_assignment` and `secondary_location_preferences`, always),\n\"You sign in through your organization, so there is no password to set\"\n(`security_setup` for an SSO person), or \"Your company doesn't use this\nstep\".\n\n**Required Scopes:** `read:own_profile` — the person's own first-run\nsetup (#7183, owner rule 2026-10-06 \"every api/v1 door asks a scope\").\nA token without it gets `403 insufficient_permissions` (\"Required scope:\nread:own_profile\") before anything else is checked. The sign-in tokens\nhold it; a browser session is asked no scope.\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 including name and contact details."
                    },
                    "skipped": {
                      "type": "boolean",
                      "description": "Present (true) only when the step is skipped for this person",
                      "example": true
                    },
                    "skipped_reason": {
                      "type": "string",
                      "description": "Why the step is skipped; present only on a skipped step",
                      "example": "Your company doesn't use this step"
                    },
                    "message": {
                      "type": "string",
                      "description": "Present only on a skipped step",
                      "example": "This step has been skipped: Your company doesn't use this step."
                    },
                    "form_fields": {
                      "type": "array",
                      "description": "Form fields for this step (absent on a skipped step)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "job_title"
                          },
                          "type": {
                            "type": "string",
                            "description": "text, email, tel, password, number, boolean, select or multi_select",
                            "example": "text"
                          },
                          "required": {
                            "type": "boolean",
                            "example": false
                          },
                          "label": {
                            "type": "string",
                            "example": "Job Title"
                          },
                          "read_only": {
                            "type": "boolean",
                            "description": "The step shows this field and saves no value for it",
                            "example": true
                          },
                          "help_text": {
                            "type": "string",
                            "description": "Present on some fields (a read-only field's reason, a number field's range)",
                            "example": "Your job title is managed by your administrator."
                          },
                          "min": {
                            "type": "integer",
                            "description": "Number fields with a range only (hours 0, consecutive days 1)",
                            "example": 0
                          },
                          "max": {
                            "type": "integer",
                            "description": "Number fields with a range only (hours 80, consecutive days 7)",
                            "example": 80
                          },
                          "options": {
                            "type": "array",
                            "description": "Select fields only",
                            "items": {
                              "type": "object",
                              "properties": {
                                "value": {
                                  "example": "alternating"
                                },
                                "label": {
                                  "type": "string",
                                  "example": "Prefer every other weekend"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "current_data": {
                      "type": "object",
                      "description": "Current values for form fields",
                      "example": {
                        "first_name": "John",
                        "last_name": "Doe",
                        "email": "john.doe@example.com",
                        "userid": null,
                        "phone": "+12065550100",
                        "job_title": "Shift Lead",
                        "time_zone": "Eastern Time (US & Canada)"
                      }
                    },
                    "validation_rules": {
                      "type": "object",
                      "description": "Validation rules for form fields",
                      "example": {
                        "first_name": {
                          "required": true,
                          "min_length": 1
                        },
                        "last_name": {
                          "required": true,
                          "min_length": 1
                        },
                        "time_zone": {
                          "required": true
                        }
                      }
                    },
                    "help_text": {
                      "type": "string",
                      "nullable": true,
                      "description": "Help text for this step",
                      "example": "This information helps us personalize your experience and ensures you receive important communications."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Step not accessible, or the token lacks `read:own_profile` (`insufficient_permissions`).",
            "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\n**`profile_completion`** saves `user.first_name`, `last_name`, `phone`\nand `time_zone`. `email` is not written. `job_title` is read-only: a\nposted title that differs from the one the step published is not\nsaved, the rest of the step still saves, and the reply names it in\n`blocked_fields: [\"job_title\"]`. Changing a verified `phone` ends its\nverification; where the organization sends texts and the person has\nnot switched them off, the reply says so in `warnings`.\n\n**`availability_shift_preferences`** (under\n`availability_shift_preferences` or `user`) is refused with 422 when:\n- `min_hours_weekly` or `max_hours_weekly` is not a whole number —\n  \"Minimum hours per week must be a whole number between 0 and 80\" /\n  \"Maximum hours per week must be a whole number between 0 and 80\";\n- `min_hours_weekly` or `max_hours_weekly` is outside 0–80 —\n  \"Minimum hours per week must be between 0 and 80\" / \"Maximum hours\n  per week must be between 0 and 80\";\n- the minimum that will be stored is above the maximum that will be\n  stored — \"Minimum hours cannot be greater than maximum hours\". An\n  end that is not posted, or posted blank, is the one already stored;\n- `max_consecutive_days` is not a whole number — \"Maximum consecutive\n  days must be a whole number between 1 and 7\";\n- `max_consecutive_days` is outside 1–7 — \"Maximum consecutive days\n  must be between 1 and 7\";\n- `weekend_availability` is not `all`, `alternating`, `occasional` or\n  `none`, or `holiday_availability` is not `all`, `some` or `none` —\n  \"… must be one of: <the option labels>\".\n\nA value posted back exactly as the person already holds it is never\nrefused, whatever it is.\n\nThe older words `never`, `sometimes` and `always` are still accepted and\nstored as `none`, `occasional` (weekend) / `some` (holiday) and `all`.\nNothing is clamped. A blank is no answer: a blank\n`max_consecutive_days`, `weekend_availability` or\n`holiday_availability` clears the stored answer, and a blank or null\n`min_hours_weekly` / `max_hours_weekly` leaves the stored hours as\nthey are.\n\n**`notification_preferences`** records `email_notifications` and\n`sms_notifications`. `shift_reminder_hours`, `quiet_hours_start` and\n`quiet_hours_end` are still accepted from older apps but are not\npublished as fields.\n\n**A skipped step** answers 200 with `step_completed: true`,\n`step_skipped: true` and `message: \"Step has been automatically\nskipped: <reason>\"` (reasons as on `GET /onboarding/steps/{step_name}`)\nand saves nothing.\n\n**A refused save** answers 422 `step_processing_failed`; `error.message`\nis the first reason and `error.details` lists them all.\n\n**Required Scopes:** `update:own_profile` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\") — and, for the `security_setup` step,\nwhich sets the password, `write:connections` on top: the scope every door\nthat changes a credential asks (#5087). A token without one gets\n`403 insufficient_permissions` (\"Required scope: …\") before anything is\nsaved. The sign-in tokens hold all of them; a browser session is asked no\nscope.\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",
                    "phone": "+12065550100",
                    "time_zone": "Eastern Time (US & Canada)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Step completed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "step_completed": {
                      "type": "boolean",
                      "example": true
                    },
                    "step_skipped": {
                      "type": "boolean",
                      "description": "Present (true) only when the step is skipped for this person and nothing was saved",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "Present only with `step_skipped`",
                      "example": "Step has been automatically skipped: Your company doesn't use this step"
                    },
                    "current_step": {
                      "type": "string",
                      "nullable": true,
                      "description": "Step the person is on after this save. `null` once `onboarding_completed` is true.",
                      "example": "notification_preferences"
                    },
                    "progress_percentage": {
                      "type": "integer",
                      "example": 25
                    },
                    "onboarding_completed": {
                      "type": "boolean",
                      "example": false
                    },
                    "next_step": {
                      "type": "string",
                      "nullable": true,
                      "description": "The next step after this one that is neither skipped nor already completed",
                      "example": "notification_preferences"
                    },
                    "next_action": {
                      "type": "string",
                      "nullable": true,
                      "example": "/api/v1/onboarding/steps/notification_preferences"
                    },
                    "blocked_fields": {
                      "type": "array",
                      "description": "Posted fields the step did not save. Present only when there is one — today `job_title`, when a title other than the published one was posted. The step itself saved.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "job_title"
                      ]
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Sentences to show the person about what the save changed. Present only when there is one — today, on `profile_completion`, when a verified phone number was changed or removed and text notifications to the person have stopped. The step itself saved.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Text notifications are off until you verify your new number in Account Settings › Notifications."
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`step_not_accessible`: previous steps are not yet completed.\n\n`impersonation_cannot_complete_account_setup`: the request uses an impersonation token — no step is saved under one.\n\n`insufficient_permissions`: the token lacks `update:own_profile`, or —\non the `security_setup` step — `write:connections`.\n\n`impersonation_cannot_change_account_security`: the request uses an impersonation token and either the step is `security_setup`, or the step is `profile_completion` and `user` carries a `phone`, `email` or `userid` value that differs from the stored one (sending the stored values back unchanged is not refused).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`step_not_found`: the step name is not an onboarding step.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The step was not saved (`step_processing_failed`, or `step_completion_failed` / `step_skip_failed`). For `step_processing_failed`, `error.message` is the first reason and `error.details` lists every reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "step_processing_failed",
                    "message": "Maximum hours per week must be between 0 and 80",
                    "details": [
                      "Maximum hours per week must be between 0 and 80"
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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\nThe fields, ranges, option values and the skipped-step reply are those of\n`GET /onboarding/steps/{step_name}`: `job_title` is read-only, there is\nno profile photo field, and the notification step publishes Email and\nSMS only. Tips and suggestions name only what the step's form offers.\n\n**Required Scopes:** `read:own_profile` — the person's own first-run\nsetup (#7183, owner rule 2026-10-06 \"every api/v1 door asks a scope\").\nA token without it gets `403 insufficient_permissions` (\"Required scope:\nread:own_profile\") before anything else is checked. The sign-in tokens\nhold it; a browser session is asked no scope.\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 including name and contact details."
                    },
                    "form_fields": {
                      "type": "array",
                      "description": "Enhanced form fields with smart suggestions. Each also carries `read_only`, and `help_text` / `min` / `max` / `options` where the plain step endpoint does.",
                      "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"
                          },
                          "read_only": {
                            "type": "boolean",
                            "example": false
                          },
                          "suggested_value": {
                            "type": "string",
                            "nullable": true,
                            "description": "On `time_zone` only",
                            "example": "Pacific Time (US & Canada)"
                          },
                          "confidence": {
                            "type": "string",
                            "enum": [
                              "medium",
                              "low"
                            ],
                            "description": "On `time_zone` only — `medium` when the offered zone was detected, else `low`",
                            "example": "medium"
                          },
                          "smart_help": {
                            "type": "string",
                            "nullable": true,
                            "description": "Field-specific help, when the field has one"
                          }
                        }
                      }
                    },
                    "current_data": {
                      "type": "object",
                      "description": "Current values with smart defaults applied",
                      "example": {
                        "first_name": "John",
                        "last_name": "Doe",
                        "email": "john.doe@example.com",
                        "userid": null,
                        "phone": null,
                        "job_title": "Shift Lead",
                        "time_zone": "Pacific Time (US & Canada)"
                      }
                    },
                    "ai_suggestions": {
                      "type": "object",
                      "description": "Suggestions and insights; the keys depend on the step (these are `profile_completion`'s; empty for steps with none)",
                      "properties": {
                        "timezone_confidence": {
                          "type": "string",
                          "example": "medium"
                        },
                        "profile_completeness_score": {
                          "type": "integer",
                          "example": 75
                        },
                        "suggested_improvements": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "Complete your phone number for emergency contact"
                          ]
                        }
                      }
                    },
                    "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",
                          "description": "Empty when the person has no role",
                          "example": {
                            "role_name": "Software Engineer",
                            "common_locations": []
                          }
                        }
                      }
                    },
                    "similar_employee_patterns": {
                      "type": "object",
                      "description": "Patterns from similar employees; an empty object when there is nobody to compare with",
                      "properties": {
                        "common_availability_patterns": {
                          "type": "object",
                          "example": {
                            "avg_min_hours": 20,
                            "avg_max_hours": 40
                          }
                        },
                        "typical_completion_time": {
                          "type": "string",
                          "nullable": true,
                          "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": [
                        "Setting your correct timezone ensures accurate scheduling",
                        "Complete contact information helps with emergency situations"
                      ]
                    },
                    "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"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Step not accessible, or the token lacks `read:own_profile` (`insufficient_permissions`).",
            "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\nIt refuses what saving the step refuses — for\n`availability_shift_preferences`, hours that are not a whole number or\nare outside 0–80, consecutive days outside 1–7, a minimum above the\nmaximum, and a weekend or holiday answer that is not one of the options\n(`never` / `sometimes` / `always` are still accepted); a value the\nperson already holds, posted back unchanged, is never refused. A `job_title` other than the published one is a\nwarning, not an error: \"Your job title is managed by your\nadministrator, so it can't be changed here.\" A skipped step answers\n200 with `valid: true`, `skipped: true` and `message`.\n\n**Required Scopes:** `update:own_profile`, like saving the step (#7183) —\nand, for the `security_setup` step, `write:connections` on top. A token\nwithout one gets `403 insufficient_permissions` (\"Required scope: …\").\nThe sign-in tokens hold all of them; a browser session is asked no scope.\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",
                    "time_zone": "Eastern Time (US & Canada)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Validation successful"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-blocking warnings; they ride a valid answer too (absent on a skipped step)",
                      "items": {
                        "type": "string"
                      },
                      "example": []
                    },
                    "skipped": {
                      "type": "boolean",
                      "description": "Present (true) only when the step is skipped for this person"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `update:own_profile`, or — on the `security_setup` step — `write:connections` (`insufficient_permissions`); or the step is `security_setup` and the request uses an impersonation token (`impersonation_cannot_change_account_security`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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": {
                        "max_hours_weekly": [
                          "Maximum hours per week must be between 0 and 80"
                        ],
                        "max_consecutive_days": [
                          "Maximum consecutive days must be between 1 and 7"
                        ]
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-blocking validation warnings",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Your job title is managed by your administrator, so it can't be changed here."
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/reset": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Reset onboarding progress",
        "description": "Resets the user's onboarding progress back to the beginning. The steps\nthat apply are asked again (the password step is never re-opened), and\nthe person's answers are kept — each step comes back with what they\nlast answered. Requires confirmation. Refused under an impersonation\ntoken.\n\n**Required Scopes:** `update:own_profile` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). Starting over re-opens the person's\nown setup, which they can redo, so it asks no delete-level scope. A token\nwithout it gets `403 insufficient_permissions` (\"Required scope:\nupdate:own_profile\"). The sign-in tokens hold it; a browser session is\nasked no scope.\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": "Account setup has been reset. Your answers are kept, so you can change them as you go."
                    },
                    "current_step": {
                      "type": "string",
                      "description": "The first step that applies to this person",
                      "example": "profile_completion"
                    },
                    "next_action": {
                      "type": "string",
                      "example": "/api/v1/onboarding/steps/profile_completion"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks `update:own_profile` (`insufficient_permissions`), or the request uses an impersonation token (`impersonation_cannot_change_account_security`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`reset_failed`: the reset could not be saved; `error.details` lists the reasons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/forms": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List published forms (Forms to Fill Out page)",
        "description": "Returns all published forms for the business, matching exactly what appears\non the web **Forms to Fill Out** 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 filters by one submission status:\n\n| `status` value | Meaning |\n|---|---|\n| _(omit)_ or `all` | All Statuses |\n| `draft` | Saved Drafts |\n| `pending_completion` | Paused for someone else to complete |\n| `submitted` | Submitted |\n| `under_review` | Under Review |\n| `changes_requested` | Fields returned for correction |\n| `approved` | Approved |\n| `rejected` | Rejected |\n| `completed` | Every finished submission — approved, or sent on a form that needs no approval (what the web pages call Completed) |\n\nEach item's `status` is the stored status; `display_state` and\n`status_label` are what the web pages show — `completed` / \"Completed\"\nfor a sent form that needs no approval.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Status filter. Omit or pass `all` for every status.\nAllowed values: all, draft, pending_completion, submitted, under_review,\nchanges_requested, approved, rejected, completed, pending_review\n(every sent submission still waiting on a decision).\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "draft",
                "pending_completion",
                "submitted",
                "under_review",
                "changes_requested",
                "approved",
                "rejected",
                "completed",
                "pending_review"
              ],
              "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
                        },
                        "completed": {
                          "type": "integer",
                          "example": 3,
                          "description": "Every finished submission — what status=completed lists"
                        },
                        "pending_review": {
                          "type": "integer",
                          "example": 1,
                          "description": "Every sent submission still waiting on a decision — what status=pending_review lists"
                        }
                      },
                      "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": "object",
                      "properties": {
                        "code": {
                          "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 Forms to Fill Out 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- **Who can open it.** Admins and Forms App Admins; managers, while the\n  Forms Manage tier includes them; a form's owner, owning team or\n  co-owner; anyone named as an approver on an active approval workflow,\n  or on the step a submission is waiting on; and anyone with a direct\n  report whose submission is waiting for a decision. Everyone else gets\n  `403`.\n- **What each caller sees:**\n  - **Admins and Forms App Admins** see the business-wide set awaiting a\n    decision (statuses `submitted` and `under_review` on forms that\n    require approval).\n  - **Everyone else** sees that set narrowed to the rows they can decide —\n    their reports', forms they own or co-own, and rows waiting on a step\n    that names them.\n- A person a form's approval workflow names for the step a submission is\n  on also sees that submission here; under a workflow only the current\n  step's approver may approve it (others may reject or return fields).\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
                        }
                      }
                    },
                    "theme": {
                      "type": "object",
                      "description": "The colour picked in the form's Design modal (a custom palette's Brand primary, or the chosen preset's). Both values are `null` when the form follows the business theme — keep the business branding.",
                      "properties": {
                        "brand_primary": {
                          "type": "string",
                          "nullable": true,
                          "description": "The picked brand colour as a",
                          "example": "#f5c518"
                        },
                        "brand_primary_fill": {
                          "type": "string",
                          "nullable": true,
                          "description": "The same colour moved until white button text clears it (WCAG AA, 4.5:1) — the fill the phone web fill page paints behind its buttons, links and checked controls. `null` when the form follows the business theme.",
                          "example": "#7d6302"
                        }
                      }
                    },
                    "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): send\nthe request as `multipart/form-data` and put the FILE PART in the\nfield's own key — `submission[submission_data][site_photo]=@photo.jpg`\nfor a single field, `submission[submission_data][damage_photos][]=@…`\n(repeated) for a gallery. Works with every authentication method,\nBearer tokens included. The same shape is accepted by\n`POST /forms/{id}/submissions` and `PATCH /form_submissions/{id}`.\nThe server validates every file (all-or-nothing, 422\n`invalid_attachments` on an unsupported type, a file over the field's\nsize limit, a file posted into a field that does not take one, or a\nfile posted under a key that is not a field on this form at all), caps\none request at 50 file parts, stores them, creates the file records, and\nrewrites those keys to a\n`{ uploaded, file_id, filename, content_type, file_size }` reference.\n\nA payload that carries NOTHING but file parts leaves every stored answer\nalone — it reads as \"add these files\", not as \"replace the answers with\nnothing\". Clearing a draft's answers is an explicit\n`submission_data: {}` (JSON), as it always was.\nValues already in reference form are left as-is (re-send them to keep a\ngallery's existing photos). A blob `signed_id` string is also accepted\nin the same key for a client that already holds one; the generic\n`POST /rails/active_storage/direct_uploads` endpoint is disabled (403)\nand is not a way to obtain one.\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"
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same body, form-encoded — and the ONLY content type a file part can be sent on (a `format: binary` property inside a JSON-only requestBody is something no generated client, and no Swagger UI, can post). Each field takes its bracketed name: `submission[submission_data][full_name]` for an answer, `submission[submission_data][site_photo]` for a single file, repeated `submission[submission_data][damage_photos][]` for a gallery. At most 50 file parts per request.",
                "properties": {
                  "submission[submission_data][<media_field>]": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file for a single-file media field (file / image / video / audio / signature). The server validates it (all-or-nothing), stores it, creates the file record and rewrites the key to a `{ uploaded, file_id, filename, content_type, file_size }` reference. A file posted under a key that is not a media field on this template is refused with 422 `invalid_attachments`."
                  },
                  "submission[submission_data][<gallery_field>][]": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Repeated file parts for a gallery / multi-file field. Send the already-uploaded photos' reference hashes back in the same key to keep them."
                  },
                  "submission[submission_data][<text_field>]": {
                    "type": "string",
                    "description": "Any non-media answer, form-encoded."
                  }
                }
              }
            }
          }
        },
        "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"
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same body, form-encoded — and the ONLY content type a file part can be sent on (a `format: binary` property inside a JSON-only requestBody is something no generated client, and no Swagger UI, can post). Each field takes its bracketed name: `submission[submission_data][full_name]` for an answer, `submission[submission_data][site_photo]` for a single file, repeated `submission[submission_data][damage_photos][]` for a gallery. At most 50 file parts per request.",
                "properties": {
                  "submission[submission_data][<media_field>]": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file for a single-file media field (file / image / video / audio / signature). The server validates it (all-or-nothing), stores it, creates the file record and rewrites the key to a `{ uploaded, file_id, filename, content_type, file_size }` reference. A file posted under a key that is not a media field on this template is refused with 422 `invalid_attachments`."
                  },
                  "submission[submission_data][<gallery_field>][]": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Repeated file parts for a gallery / multi-file field. Send the already-uploaded photos' reference hashes back in the same key to keep them."
                  },
                  "submission[submission_data][<text_field>]": {
                    "type": "string",
                    "description": "Any non-media answer, form-encoded."
                  }
                }
              }
            }
          }
        },
        "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"
                        },
                        {
                          "$ref": "#/components/schemas/FormSubmissionDecisionCapabilities"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "can_approve": {
                              "type": "boolean",
                              "description": "May this caller approve it now (`Forms::SubmissionAccess#approvable?`) — false for someone who may not decide it, on a decided row, and for a reviewer whose workflow step it is not. Needs `write:forms`.\n"
                            },
                            "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. Begins\nwith a \"Form Submitted\" event once sent, followed by a single\nreview event reflecting the current state (approved / fields\nreturned for correction / rejected) when reviewed. A draft its\nsender withdrew carries a `withdrawn` event; a submission\nwaiting for a decision again carries `reopened_for_review`\n(an approval taken back) and/or `returns_taken_back` (a return\nfor correction taken back), each with who did it and their\nreason in `note`.\n",
                      "items": {
                        "$ref": "#/components/schemas/SubmissionTimelineEvent"
                      }
                    },
                    "workflow_step_runs": {
                      "type": "array",
                      "description": "What each step of the form's workflow did for this submission,\nnewest first, at most 20: the \"Workflow steps\" card on the web and\nphone submission pages. Only people who manage the form see it\n(admins, Forms app admins, and the form's creator, owner, owning\nteam and co-owners); for everyone else it is an empty array.\n",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "step_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "The step's name; null for an unnamed step."
                          },
                          "step_label": {
                            "type": "string",
                            "description": "What the web card prints for the step — its name, or \"Unnamed step\" in the reader's language.",
                            "example": "Email the safety lead"
                          },
                          "outcome": {
                            "type": "string",
                            "enum": [
                              "done",
                              "skipped",
                              "failed"
                            ]
                          },
                          "outcome_label": {
                            "type": "string",
                            "example": "Skipped"
                          },
                          "detail": {
                            "type": "string",
                            "nullable": true,
                            "description": "What the step produced, why it was skipped, or the error (up to 500 characters)."
                          },
                          "trigger_status": {
                            "type": "string",
                            "description": "The submission status that ran the step.",
                            "example": "approved"
                          },
                          "trigger_label": {
                            "type": "string",
                            "description": "That status in words, in the reader's language; the web card reads it as \"When approved\".",
                            "example": "Approved"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the owner and not an admin/manager/Forms App Admin\n(`forbidden`). Someone the submission ASKED to decide it who may no\nlonger open it is told why instead: that it was withdrawn, returned,\nrejected or approved (\"Nothing on this submission is waiting on you\nany more — it was rejected.\"), or that their own access to it ended.\n"
          },
          "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 the submitter's own draft or rejected submissions can be updated.\nA submission returned for changes (`changes_requested`) answers 422\n`returned_report_no_draft` — send the corrections with\n`POST /forms/{id}/submit`. A draft on a form that is no longer\naccepting submissions answers 403 `form_not_available`, and one whose\naudience no longer includes the caller 403 `access_denied`.\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"
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same body, form-encoded — and the ONLY content type a file part can be sent on (a `format: binary` property inside a JSON-only requestBody is something no generated client, and no Swagger UI, can post). Each field takes its bracketed name: `submission[submission_data][full_name]` for an answer, `submission[submission_data][site_photo]` for a single file, repeated `submission[submission_data][damage_photos][]` for a gallery. At most 50 file parts per request.",
                "properties": {
                  "submission[submission_data][<media_field>]": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file for a single-file media field (file / image / video / audio / signature). The server validates it (all-or-nothing), stores it, creates the file record and rewrites the key to a `{ uploaded, file_id, filename, content_type, file_size }` reference. A file posted under a key that is not a media field on this template is refused with 422 `invalid_attachments`."
                  },
                  "submission[submission_data][<gallery_field>][]": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Repeated file parts for a gallery / multi-file field. Send the already-uploaded photos' reference hashes back in the same key to keep them."
                  },
                  "submission[submission_data][<text_field>]": {
                    "type": "string",
                    "description": "Any non-media answer, form-encoded."
                  }
                }
              }
            }
          }
        },
        "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:** the caller must be able to decide this submission —\nthe rule the web uses (`Forms::SubmissionAccess#decidable?`): an admin or\nForms App Admin, one of the form's own people (owner, owning team,\nco-owners, or its creator while inside the Forms Manage tier), the\nsubmitter's manager, or a person the form's approval workflow names for\nthe step the submission is on (`write:forms` scope). A person named on\nanother step of its workflow receives `403` with code\n`not_current_approver`. Its message names the step the submission is\nwaiting on and that step's approvers only to a caller who may read the\nsubmission; otherwise it says the submission isn't at their step yet,\nor that it isn't waiting on them. Anyone else 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:** the caller must be able to decide this submission —\nthe rule the web uses (`Forms::SubmissionAccess#decidable?`): an admin or\nForms App Admin, one of the form's own people (owner, owning team,\nco-owners, or its creator while inside the Forms Manage tier), the\nsubmitter's manager, or a person the form's approval workflow names for\nthe step the submission is on (`write:forms` scope). A person named on\nanother step of its workflow receives `403` with code\n`not_current_approver`. Its message names the step the submission is\nwaiting on and that step's approvers only to a caller who may read the\nsubmission; otherwise it says the submission isn't at their step yet,\nor that it isn't waiting on them. Anyone else 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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:forms` scope.\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:** the caller must be able to decide this submission —\nthe rule the web uses (`Forms::SubmissionAccess#decidable?`): an admin or\nForms App Admin, one of the form's own people (owner, owning team,\nco-owners, or its creator while inside the Forms Manage tier), the\nsubmitter's manager, or a person the form's approval workflow names for\nthe step the submission is on (the token must carry `write:forms`). A\nperson named on another step of its workflow receives `403` with code\n`not_current_approver`. Its message names the step the submission is\nwaiting on and that step's approvers only to a caller who may read the\nsubmission; otherwise it says the submission isn't at their step yet,\nor that it isn't waiting on them. Anyone else receives `403`.\n\n**Preconditions:**\n- The submission must be **awaiting review** (`submitted` or `under_review`),\n  else `422` with code `not_awaiting_review`.\n- There must be someone to return it to. An **anonymous** submission\n  (public portal) returns `422` with code `anonymous_submission`; one\n  sent without an account (a candidate's assessment, a job application)\n  `submitter_has_no_account`; a submitter who is no longer an active\n  member of the organization `submitter_not_member`; a customer's\n  answers to an Implementation Hub project's form request go back\n  through the request (it is reopened and the customer told), and once\n  no request holds them or the project is closed to the customer the\n  call returns `request_closed`, or `submitter_left_team` when the\n  person whose copy it is has left the customer's team — approve or\n  reject it instead. `can_return_fields` on the submission says which.\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), every other answered field this reviewer may read marked\n`accepted` (named in `accepted_fields`), and `reviewed_by` / `reviewed_at`\nstamped. 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 answers 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`) or without an account\n(`submitter_has_no_account`), its submitter is no longer a member\n(`submitter_not_member`) or no longer on the customer's team\n(`submitter_left_team`), its project's form request is closed\n(`request_closed`), 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"
                }
              }
            }
          }
        }
      }
    },
    "/form_submissions/{id}/withdraw": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Withdraw a submission waiting for review",
        "description": "The submitter takes back their own submission while it waits for a\ndecision; it becomes a draft again to correct and send\n(`MaForms::FormSubmission#withdraw!` — the web's **Withdraw**). The\nreviewers' open asks close. Refused (`422 cannot_withdraw`, with the\nreason as the message and the current `status` in `error.details`)\nwhen the caller did not send it, it is not waiting for review, an\napproval-workflow step was already signed, it was filed to HR Files,\nit belongs to an onboarding plan item, or its form no longer accepts\nresponses. `can_withdraw` / `withdraw_refusal` on the submission say\nwhich in advance. Requires `write:forms`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Form submission ID",
            "schema": {
              "type": "integer",
              "example": 4812
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn — the submission is a draft again",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Submission withdrawn — it is a draft again"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The submission can't be withdrawn (`cannot_withdraw`) or could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/form_submissions/{id}/reopen": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Reopen an approved submission for review",
        "description": "Takes an approval made by mistake back to **Under review**\n(`MaForms::FormSubmission#reopen_for_review!` — the web's **Reopen for\nreview**). `reason` is required; the submitter is told it, and whoever\nmay decide the submission now is asked again (not the caller, unless\nnobody else may). Refused (`422 cannot_reopen`, the reason as the\nmessage) unless the caller may approve it and the approval has had no\neffect reopening cannot undo — a multi-step workflow's sign-offs, a\ndocument filed to HR Files, a verified onboarding item, or a workflow\nrule for approved submissions that already ran. `can_reopen` /\n`reopen_refusal` on the submission say which in advance. Requires\n`write:forms`.\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": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Why it is reopened — shown to the submitter.",
                    "example": "Approved the wrong row from the list."
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reopened — the submission waits for a decision again",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Submission reopened for review"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The submission can't be reopened (`cannot_reopen`), `reason` is\nmissing (`reason_required`), or it could not be saved.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/form_submissions/{id}/take_back_returns": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Take back a return for correction",
        "description": "A reviewer takes back a return for correction\n(`MaForms::FormSubmission#take_back_returns!` — the web's **Take back\nthe return**): the submission waits for a decision again with its\nanswers as sent, the submitter's \"correct these answers\" ask closes and\nthey are told why, and whoever may decide it now is asked (not the\ncaller, unless nobody else may). `reason` is required. Refused\n(`422 cannot_take_back`, the reason as the message) unless it is\nreturned for correction and the caller manages it or is the reviewer\nwho returned it while still signing its current step — and never on an\nonboarding plan item's form (the new hire corrects it in their\nonboarding plan). `can_take_back_returns` / `take_back_returns_refusal`\non the submission say which in advance. Requires `write:forms`.\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": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Why the return is taken back — shown to the submitter.",
                    "example": "Returned the wrong answers — they were fine."
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return taken back — the submission is under review again",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Return taken back — the submission is under review again"
                    },
                    "submission": {
                      "$ref": "#/components/schemas/ReviewedSubmission"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The return can't be taken back (`cannot_take_back`), `reason` is\nmissing (`reason_required`), or it could not be saved.\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 the business's app order (the apps its admin numbered in\nSort Order first, then the rest by the catalogue's rank), the order the web lists\nthem in; each app's `sort_order` is its place in that order, starting at 1. They\ninclude configuration data specific to the business.\n\n**Required Scopes:** `read:organization` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made by\nhand before this deploy may not. A browser session is asked no scope.\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\n**Required Scopes:** `read:organization` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made by\nhand before this deploy may not. A browser session is asked no scope.\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\n**Required Scopes:** `read:organization` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made by\nhand before this deploy may not. A browser session is asked no scope.\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 CURATED SUBSET of the app's web\n  LHS sidebar — six tabs, each shown under the SAME visibility gating\n  it has on the web rail: if the tab is hidden on web for this caller\n  it is withheld here too. Two things gate a tab — its tenant SURFACE\n  TOGGLE (so a disabled surface never leaves a tab that 403s) and, for\n  the author-tier tabs, the caller's role. The tabs, in web order:\n\n    * `my_day` (\"My Day\", `/apps/frontline-execution/my-day`) — every\n      user; requires `enable_my_day`.\n    * `requests` (\"My requests\",\n      `/apps/frontline-execution/requests`) — every user; requires\n      `enable_campaigns`.\n    * `day_sheet` (\"Day Sheet\",\n      `/apps/frontline-execution/day-sheet`) — the AUTHOR TIER (a\n      business admin, the Frontline Execution app admin, or any\n      manager); requires `enable_my_day`.\n    * `reviews` (\"Reviews\", `/apps/frontline-execution/reviews`) —\n      the author tier OR a completion **reviewer**; requires\n      `enable_my_day`.\n    * `campaigns` (\"Campaigns\",\n      `/apps/frontline-execution/campaigns`) — the author tier;\n      requires `enable_campaigns`.\n    * `coverage` (\"Coverage\", `/apps/frontline-execution/coverage`) —\n      the author tier; requires `enable_coverage`.\n\n  `my_day` shows for every frontline user with no role gate (its\n  endpoints — `GET /api/v1/frontline_execution/my_day` and\n  `.../my_day/everything` — carry no author-tier gate either), so head\n  office / district / regional users reach their own daily surface too.\n  Paths are the web rail's own desktop `/apps/...` paths. No item\n  carries a badge count. The rest of the web rail — Overview, Request\n  queue, Calendar, People, Shared blockers, Analytics and the Admin\n  zone (Settings / Import / Workforce sources) — is not part of this\n  bar.\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* **Service Desk** returns the per-persona bottom bar from the\n  Service Desk mobile prototype. The persona is the caller's Service\n  Desk role: **admin** (business admin or Service Desk app admin),\n  **agent** (member of an active support team, or granted agent\n  rights in Settings → Agents), **manager** (help\n  desk manager without agent rights) or **member** (everyone else).\n\n    * **member** — `service_desk_my_requests` (\"My Requests\"),\n      `service_desk_grievances` (\"Grievances\",\n      `/apps/service-desk/grievances`) and\n      `service_desk_knowledge_base` (\"Knowledge\",\n      `/apps/service-desk/knowledge-base`).\n    * **manager** — `service_desk_my_requests`,\n      `service_desk_access_requests` (\"Access\",\n      `/apps/service-desk/access-requests`),\n      `service_desk_grievances` and `service_desk_knowledge_base`.\n    * **agent** and **admin** — `service_desk_agent_dashboard`\n      (\"Dashboard\", `/apps/service-desk/agent_dashboard`),\n      `service_desk_tickets` (\"Tickets\" — an agent's own workbench\n      `/apps/service-desk/agent_tickets`; for an admin the\n      tenant-wide `/apps/service-desk/tickets`),\n      `service_desk_knowledge_base`, and `service_desk_more` (\"More\").\n      `service_desk_more` has no `path`; its `actions` list the\n      enabled overflow modules — `service_desk_problems`\n      (`/apps/service-desk/problems`), `service_desk_changes`\n      (`/apps/service-desk/changes`), `service_desk_access_requests`\n      and `service_desk_grievances` — and each action carries its own\n      `key`. When only one of them is enabled it is returned as a\n      normal item instead of `service_desk_more`; when none is, there\n      is no More item.\n\n  `service_desk_my_requests` is `/m/apps/service-desk` for a mobile\n  caller and `/apps/service-desk/my_requests` otherwise. Knowledge\n  follows the tenant's `knowledge_base_enabled` setting; Access,\n  Problems, Changes and Grievances follow their Service Desk modules.\n  `service_desk_grievances` is the app's door to file a grievance\n  (GET /service_desk/service_catalog never lists that type); render\n  it from GET /service_desk/grievances. Keys are namespaced\n  `service_desk_*`. Analytics, Settings and CMDB are not part of this\n  navigation, and no item carries a badge count.\n* **Sites** returns the bar from the Sites mobile prototype — the web\n  rail's reader rows, in its order, each under the SAME gate the web\n  rail applies (asked of the web's own Sites predicates, so the two\n  cannot disagree about a person):\n\n    * `sites_dashboard` (\"Dashboard\", `/apps/sites`, icon\n      `fas fa-gauge`) — only when the Dashboard has work for the\n      caller: a business admin, a Sites app admin, someone who may\n      create sites or owns / edits one, a page reviewer, or the owner\n      of a page. A member who only reads has no Dashboard (on the web\n      they land on All sites).\n    * `sites_all_sites` (\"All sites\", `/apps/sites/list`,\n      `fas fa-globe`) — always.\n    * `sites_recent` (\"Recent\", `/apps/sites/recent`,\n      `fas fa-clock-rotate-left`) — while the tenant records page\n      views (`enable_analytics`, on by default).\n    * `sites_saved` (\"Saved & following\", `/apps/sites/bookmarks`,\n      `fas fa-bookmark`) — always, with the same label for every\n      role. `count` is the number of must-read pages waiting on the\n      caller's confirmation; while it is above 0 the path opens on\n      that list (`/apps/sites/bookmarks?tab=confirm`). `tabs` is the\n      screen's segmented control, the web's tabs in order — `{ key,\n      title }`, `count` on `confirm` — each key naming its list:\n      `bookmarks` → `GET /sites/bookmarks`, `following` → `GET\n      /sites/following`, `watching` → `GET /sites/watching`,\n      `confirm` → `GET /sites/to_confirm`. Following and Watching\n      are there only while the tenant's site follows are on.\n    * `sites_approvals` (\"Approvals\", `/apps/sites/reviews`,\n      `fas fa-clipboard-check`) — only while page approvals are on\n      (`enable_content_review`) and the caller is a business admin, a\n      Sites app admin, an owner or editor of a site, or someone a\n      page approval was sent to or sent by. `count` is what waits on\n      the caller (requests to decide, their requests sent back, and\n      their approved pages ready to publish); while it is above 0 the\n      path opens on that list (`/apps/sites/reviews?filter=waiting`).\n\n  For a mobile caller the paths are the phone pages:\n  `sites_dashboard` and `sites_all_sites` → `/m/apps/sites` (the\n  phone sites list, which draws the Dashboard's sections above it),\n  `sites_recent` → `/m/apps/sites/recent`, `sites_saved` →\n  `/m/apps/sites/bookmarks` (`?tab=confirm` as above).\n  `sites_approvals` has no phone page and keeps its desktop path —\n  draw it natively by `key`. Keys are namespaced `sites_*`. The web\n  rail's \"Your sites\", Site resources and Admin groups are not part\n  of this navigation. Titles are written in the caller's language,\n  as for Service Desk.\n* **Meal Compliance** returns the per-persona bar from the Meal\n  Compliance mobile prototype, each item under the SAME gate the web\n  rail applies (asked of the web's own Meal Compliance predicates):\n\n    * `meal_compliance_dashboard` (\"Dashboard\",\n      `/apps/meal-compliance`, icon `fas fa-gauge-high`) — managers\n      and admins: a business admin, a Meal Compliance app admin, an\n      HR admin, or a manager of direct reports (a manager role, or\n      anyone people report to).\n    * `meal_compliance_my_breaks` (\"My Breaks\",\n      `/apps/meal-compliance/my-breaks`, `fas fa-mug-hot`) —\n      always: the caller's own waiver status and break history.\n    * `meal_compliance_waivers` (\"Waivers\",\n      `/apps/meal-compliance/waivers`, `fas fa-file-signature`) —\n      the same people as Dashboard.\n    * `meal_compliance_break_compliance` (\"Break Compliance\",\n      `/apps/meal-compliance/break-compliance`,\n      `fas fa-clipboard-check`) — admins only: a business admin, a\n      Meal Compliance app admin or an HR admin.\n\n  So an employee gets My Breaks; a manager Dashboard · My Breaks ·\n  Waivers; an admin all four, in that order. The paths are the\n  desktop pages for every caller, mobile included — draw each screen\n  natively by `key` (the desktop My Breaks page sends a phone to\n  `/m/apps/meal-compliance` itself). An employee's single item is\n  kept, not collapsed into the tile. Keys are namespaced\n  `meal_compliance_*`. The web rail's Settings is not part of this\n  navigation, and no item carries a badge count. Titles are written\n  in the caller's language, as for Service Desk.\n\n  Every item also carries `can_create_waiver` (boolean, the same\n  value on each item): whether the caller may start a New Waiver,\n  which the prototype offers as the (+) button on Dashboard and\n  Waivers. It is the web's New Waiver rule — a business admin, a\n  Meal Compliance app admin, an HR admin or a manager of direct\n  reports — and the token must hold `write:meal_compliance`, so a\n  read-only token answers `false` while the items stay the same. It\n  matches `permissions.can_create_waiver` on\n  `GET /meal_compliance/dashboard` and `GET /meal_compliance/waivers`.\n* **SOP Hub** returns the per-persona bar from the SOP Hub mobile\n  prototype, each item under the SAME gate the web rail applies:\n\n    * `sop_hub_dashboard` (\"Dashboard\", `/apps/sop-hub`, icon\n      `fas fa-gauge`) — always. Render it from\n      `GET /sop_hub/dashboard`.\n    * `sop_hub_my_sops` (\"My SOPs\",\n      `/apps/sop-hub/completions/my_pending`,\n      `fas fa-clipboard-check`) — always: the caller's own SOPs,\n      Pending (`GET /sop_hub/completions/my_pending`) and History\n      (`GET /sop_hub/completions/my_history`).\n    * `sop_hub_reports` (\"Reports\",\n      `/apps/sop-hub/completions/report`, `fas fa-clipboard-list`) —\n      SOP Hub authors only: an HR admin, a business admin, a SOP Hub\n      app admin, or a SOP Hub admin limited to places and\n      departments — exactly the people `GET /sop_hub/reports/compliance`,\n      `GET /sop_hub/completions` and `GET /sop_hub/activity_logs`\n      admit, so the tab never opens on a 403.\n\n  So an employee (and a manager who is not a SOP Hub author) gets\n  Dashboard · My SOPs; an admin all three, in that order. For a mobile\n  caller `sop_hub_dashboard` is `/m/apps/sop-hub` and\n  `sop_hub_my_sops` is `/m/apps/sop-hub/my-pending`;\n  `sop_hub_reports` has no phone page and keeps its desktop path —\n  draw it natively by `key`. Keys are namespaced `sop_hub_*`. The web\n  rail's Browse SOPs, Manage, Templates, Manuals and Settings are not\n  part of this navigation, and no item carries a badge count. Titles\n  are written in the caller's language, as for Service Desk.\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* An item's `title` (and an action's `title`) is display text, not an\n  identifier — clients branch on `key`. For **Service Desk**,\n  **Sites** and **Meal Compliance** the titles are written in the language the client asks for: the\n  `locale` query parameter, else `Accept-Language` (first supported\n  tag by q-value; `nb`/`nn` read as `no`, `iw` as `he`), else the\n  caller's own profile language, else the tenant's default, else\n  English — whether or not the tenant translates its web pages. The\n  response then carries `Vary: Accept-Language`. Other apps' titles\n  are English today.\n",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language the client's own interface is in (e.g. `fr`, `pt-BR`).\nTakes precedence over `Accept-Language` for the `title`s written in\nthe caller's language (see `include_navigation`). An unsupported\ncode is ignored.\n",
            "schema": {
              "type": "string"
            },
            "example": "fr"
          }
        ],
        "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": 2
                        }
                      ],
                      "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"
                            }
                          ]
                        },
                        {
                          "id": 107,
                          "slug": "sites",
                          "name": "Sites",
                          "type": "core",
                          "category": "productivity-utility",
                          "enabled": true,
                          "pinned": false,
                          "icon": {
                            "url": "fas fa-globe",
                            "type": "icon_class"
                          },
                          "url": "/apps/sites",
                          "has_mobile_view": true,
                          "mobile_url": "/m/apps/sites",
                          "navigation_items": [
                            {
                              "key": "sites_dashboard",
                              "title": "Dashboard",
                              "icon": "fas fa-gauge",
                              "path": "/apps/sites"
                            },
                            {
                              "key": "sites_all_sites",
                              "title": "All sites",
                              "icon": "fas fa-globe",
                              "path": "/apps/sites/list"
                            },
                            {
                              "key": "sites_recent",
                              "title": "Recent",
                              "icon": "fas fa-clock-rotate-left",
                              "path": "/apps/sites/recent"
                            },
                            {
                              "key": "sites_saved",
                              "title": "Saved & following",
                              "icon": "fas fa-bookmark",
                              "path": "/apps/sites/bookmarks?tab=confirm",
                              "count": 2,
                              "tabs": [
                                {
                                  "key": "bookmarks",
                                  "title": "Bookmarks"
                                },
                                {
                                  "key": "following",
                                  "title": "Following"
                                },
                                {
                                  "key": "watching",
                                  "title": "Watching"
                                },
                                {
                                  "key": "confirm",
                                  "title": "To confirm",
                                  "count": 2
                                }
                              ]
                            },
                            {
                              "key": "sites_approvals",
                              "title": "Approvals",
                              "icon": "fas fa-clipboard-check",
                              "path": "/apps/sites/reviews?filter=waiting",
                              "count": 1
                            }
                          ]
                        }
                      ],
                      "total_count": 4,
                      "pinned_count": 1,
                      "apps_count": 3,
                      "categories": [
                        "Collaboration",
                        "Knowledge",
                        "productivity-utility",
                        "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": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "validation_failed"
                        },
                        "message": {
                          "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\nOn the default shift list, explicit `start_date` / `end_date` / `date`\ntake precedence: when any of them is sent, `period` does not narrow\nthe result and only sets the sort direction (`past` = newest first).\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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 123
                              },
                              "managed_by": {
                                "type": "string",
                                "nullable": true,
                                "example": "UKG",
                                "description": "The workforce management system of record that owns this shift (\"Managed in UKG\"), or null. A managed shift is read-only here: coverage requests and edits answer 422 `externally_managed`."
                              },
                              "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"
                                  },
                                  "lister_status": {
                                    "type": "string",
                                    "enum": [
                                      "open",
                                      "filled",
                                      "traded",
                                      "closed",
                                      "cancelled"
                                    ],
                                    "description": "Present only when the listing is the caller's own. The status as its lister reads it: listing_status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                                    "example": "traded"
                                  },
                                  "lister_status_label": {
                                    "type": "string",
                                    "description": "Present only when the listing is the caller's own. The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                                    "example": "Traded"
                                  },
                                  "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": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "current_time": {
                                  "type": "string",
                                  "format": "date-time",
                                  "description": "`type=current` only: the server time the window was measured from."
                                },
                                "current_shift": {
                                  "allOf": [
                                    {
                                      "type": "object",
                                      "properties": {
                                        "id": {
                                          "type": "integer",
                                          "example": 123
                                        },
                                        "managed_by": {
                                          "type": "string",
                                          "nullable": true,
                                          "example": "UKG",
                                          "description": "The workforce management system of record that owns this shift (\"Managed in UKG\"), or null. A managed shift is read-only here: coverage requests and edits answer 422 `externally_managed`."
                                        },
                                        "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"
                                            },
                                            "lister_status": {
                                              "type": "string",
                                              "enum": [
                                                "open",
                                                "filled",
                                                "traded",
                                                "closed",
                                                "cancelled"
                                              ],
                                              "description": "Present only when the listing is the caller's own. The status as its lister reads it: listing_status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                                              "example": "traded"
                                            },
                                            "lister_status_label": {
                                              "type": "string",
                                              "description": "Present only when the listing is the caller's own. The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                                              "example": "Traded"
                                            },
                                            "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"
                                        }
                                      }
                                    }
                                  ],
                                  "nullable": true
                                },
                                "user_timezone": {
                                  "type": "string",
                                  "example": "America/New_York"
                                },
                                "type": {
                                  "type": "string",
                                  "example": "open_shifts"
                                },
                                "day_notes": {
                                  "type": "array",
                                  "description": "Your own shifts only (`type=assigned` or `user_id=current`, not\nanother person's): the schedule notes for the next 14 days of\npublished schedules at your locations or with your shifts, soonest\nfirst — the same \"Schedule notes\" My Shifts shows on the web.\n",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "date": {
                                        "type": "string",
                                        "format": "date",
                                        "example": "2025-01-28"
                                      },
                                      "body": {
                                        "type": "string",
                                        "example": "Quarterly department meeting at 2 PM"
                                      },
                                      "schedule_id": {
                                        "type": "integer"
                                      },
                                      "schedule_name": {
                                        "type": "string",
                                        "nullable": true
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                },
                "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"
                      }
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shifts"
        ],
        "summary": "Create a shift",
        "description": "Creates one shift at the organization's default location, and optionally assigns people to it.\nRequires the `write:shifts` scope, a manager-or-above role and the Shifts **Create** permission\n(a manager limited to some sites may create only where they manage).\n\n- The shift fields are wrapped in `shift`. `location_id` is NOT accepted here: sending it answers\n  400 `location_id_not_settable`. To create a shift at a specific location use\n  `POST /shifts/bulk_create` (one row is fine).\n- `user_ids` (top level, beside `shift`) assigns people; each must belong to this organization.\n  Assignees are notified after the shift is saved.\n- Coverage (`needs_coverage`) is computed from the roster after the save; it cannot be sent.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shift"
                ],
                "properties": {
                  "shift": {
                    "type": "object",
                    "description": "The shift's fields. On create, `name`, `start_time` and `end_time` are required. `location_id`\nis refused on create (400) and ignored on update; `needs_coverage` is computed, never sent.\nAn explicit `\"urgent\": null` is ignored (the flag is left as it was).\n",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string",
                        "nullable": true
                      },
                      "start_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "status": {
                        "type": "string",
                        "description": "Defaults to the shift's initial status when omitted"
                      },
                      "location_department_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "A department of the shift's location"
                      },
                      "required_staff_count": {
                        "type": "integer"
                      },
                      "urgent": {
                        "type": "boolean"
                      },
                      "shift_label_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "planned_breaks_attributes": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "An existing planned break to change (omit to add one)"
                            },
                            "break_type_id": {
                              "type": "integer"
                            },
                            "timing_type": {
                              "type": "string"
                            },
                            "scheduled_time": {
                              "type": "string"
                            },
                            "window_start": {
                              "type": "string"
                            },
                            "window_end": {
                              "type": "string"
                            },
                            "timing_preferences": {
                              "type": "object"
                            },
                            "is_required": {
                              "type": "boolean"
                            },
                            "_destroy": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  },
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "People to assign (members of this organization). Omit to leave the roster alone; on update,\nan empty array clears it.\n"
                  }
                }
              },
              "example": {
                "shift": {
                  "name": "Morning Shift",
                  "start_time": "2026-10-05T08:00:00Z",
                  "end_time": "2026-10-05T16:00:00Z",
                  "required_staff_count": 3,
                  "notes": "Truck delivery at 9"
                },
                "user_ids": [
                  101,
                  102
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shift created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "start_time",
                    "end_time",
                    "status",
                    "created_at",
                    "updated_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "status": {
                      "type": "string"
                    },
                    "location_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "location_department_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "schedule_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_sub_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "shift_type": {
                      "type": "string",
                      "nullable": true
                    },
                    "required_staff_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "needs_coverage": {
                      "type": "boolean"
                    },
                    "urgent": {
                      "type": "boolean"
                    },
                    "ends_next_day": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_open_shift": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_ad_hoc": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "location": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "first_name": {
                            "type": "string"
                          },
                          "last_name": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "planned_breaks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "break_type_id": {
                            "type": "integer"
                          },
                          "timing_type": {
                            "type": "string"
                          },
                          "scheduled_time": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_start": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_end": {
                            "type": "string",
                            "nullable": true
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "nullable": true
                          },
                          "is_required": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`location_id_not_settable` (location_id was sent — use `POST /shifts/bulk_create`),\nor the `shift` object is missing.\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"
                  ]
                },
                "example": {
                  "error": {
                    "code": "location_id_not_settable",
                    "message": "location_id cannot be set on this endpoint. Use POST /api/v1/shifts/bulk_create to create a shift at a specific location.",
                    "details": {
                      "param": "shift[location_id]"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused. `error.code` says which gate refused it, and `error.message` why:\n* `insufficient_permissions` — the token lacks `write:shifts`\n  (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Required scope: write:shifts\"}}`);\n* `forbidden` — the person's role, site or Shifts permission refused it:\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to manage shifts\"}}` (not a manager),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You do not have permission to manage shifts at this location\"}}` (another site),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"Not authorized to access this shift\"}}` (a shift they may not see; update and delete only), or\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to <create|edit|delete> shifts.\"}, \"status\": \"error\", \"success\": false, \"message\": \"…\"}`\n  (the Shifts Create / Edit / Delete permission).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/shifts/bulk_create": {
      "post": {
        "tags": [
          "Shifts"
        ],
        "summary": "Create several shifts",
        "description": "Creates each row as its own shift; a failed row does not stop the others. Unlike `POST /shifts`,\na row may name its `location_id`. Rows carry no roster: every shift is created unstaffed, so no one\nis notified. A row with a `schedule_id` in a published schedule is published at once.\nRequires `write:shifts`, a manager-or-above role and the Shifts **Create** permission.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shifts"
                ],
                "properties": {
                  "shifts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "One row of POST /shifts/bulk_create or PUT /shifts/bulk_update (unwrapped — no `shift` key).",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "start_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "status": {
                          "type": "string"
                        },
                        "location_id": {
                          "type": "integer"
                        },
                        "location_department_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "schedule_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "required_staff_count": {
                          "type": "integer"
                        },
                        "urgent": {
                          "type": "boolean"
                        },
                        "ends_next_day": {
                          "type": "boolean"
                        },
                        "is_open_shift": {
                          "type": "boolean"
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "shifts": [
                  {
                    "name": "Morning Shift",
                    "location_id": 12,
                    "start_time": "2026-10-05T08:00:00Z",
                    "end_time": "2026-10-05T16:00:00Z",
                    "required_staff_count": 3
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every row was created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "All shifts created successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_shifts_payload` — `shifts` is not an array of shift objects",
            "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": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Some rows failed. The rows in `results.success` WERE created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Some shifts failed to create"
                    },
                    "results": {
                      "type": "object",
                      "properties": {
                        "success": {
                          "type": "array",
                          "description": "IDs of the shifts that were written",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "failed": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "description": "bulk_update only"
                              },
                              "params": {
                                "type": "object",
                                "description": "bulk_create only — the row as received"
                              },
                              "errors": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "code": {
                                "type": "string",
                                "description": "`externally_managed` when the shift is managed in the workforce management system"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shifts/bulk_update": {
      "put": {
        "tags": [
          "Shifts"
        ],
        "summary": "Update several shifts",
        "description": "Updates each row (matched by `id` in this organization); a failed row does not stop the others.\nA row that moves a shift to another `schedule_id` takes that schedule's published state.\nA shift managed in the workforce management system (e.g. UKG) is refused per row with\n`code: externally_managed`. With the organization's \"edit past shifts\" setting off, a request that\ntouches a shift that has already ended in a published schedule is refused whole\n(422 `past_shift_locked`). People assigned to a materially changed shift are notified once each.\nRequires `write:shifts`, a manager-or-above role and the Shifts **Edit** permission.\n\nA row that cancels a shift (`status` set to `cancelled`) makes the call\ndelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:scheduling` scope.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shifts"
                ],
                "properties": {
                  "shifts": {
                    "type": "array",
                    "items": {
                      "allOf": [
                        {
                          "type": "object",
                          "required": [
                            "id"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "description": "One row of POST /shifts/bulk_create or PUT /shifts/bulk_update (unwrapped — no `shift` key).",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "description": {
                              "type": "string",
                              "nullable": true
                            },
                            "start_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "end_time": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "status": {
                              "type": "string"
                            },
                            "location_id": {
                              "type": "integer"
                            },
                            "location_department_id": {
                              "type": "integer",
                              "nullable": true
                            },
                            "schedule_id": {
                              "type": "integer",
                              "nullable": true
                            },
                            "required_staff_count": {
                              "type": "integer"
                            },
                            "urgent": {
                              "type": "boolean"
                            },
                            "ends_next_day": {
                              "type": "boolean"
                            },
                            "is_open_shift": {
                              "type": "boolean"
                            },
                            "notes": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              },
              "example": {
                "shifts": [
                  {
                    "id": 3901,
                    "start_time": "2026-10-05T09:00:00Z",
                    "end_time": "2026-10-05T17:00:00Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every row was updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "All shifts updated successfully"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_shifts_payload` — `shifts` is not an array of shift objects",
            "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": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Some rows failed (the rows in `results.success` WERE updated), or `past_shift_locked`\n(nothing was changed; `details.shift_ids` names the locked shifts).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "message": {
                          "type": "string",
                          "example": "Some shifts failed to create"
                        },
                        "results": {
                          "type": "object",
                          "properties": {
                            "success": {
                              "type": "array",
                              "description": "IDs of the shifts that were written",
                              "items": {
                                "type": "integer"
                              }
                            },
                            "failed": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "description": "bulk_update only"
                                  },
                                  "params": {
                                    "type": "object",
                                    "description": "bulk_create only — the row as received"
                                  },
                                  "errors": {
                                    "type": "array",
                                    "items": {
                                      "type": "string"
                                    }
                                  },
                                  "code": {
                                    "type": "string",
                                    "description": "`externally_managed` when the shift is managed in the workforce management system"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "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}": {
      "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
                        },
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record that owns this shift (\"Managed in UKG\"), or null. A managed shift is read-only here: coverage requests and edits answer 422 `externally_managed`."
                        },
                        "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"
                            },
                            "lister_status": {
                              "type": "string",
                              "enum": [
                                "open",
                                "filled",
                                "traded",
                                "closed",
                                "cancelled"
                              ],
                              "description": "Present only when the listing is the caller's own. The status as its lister reads it: listing_status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                              "example": "traded"
                            },
                            "lister_status_label": {
                              "type": "string",
                              "description": "Present only when the listing is the caller's own. The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                              "example": "Traded"
                            },
                            "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
                        },
                        "placements": {
                          "type": "array",
                          "maxItems": 1,
                          "description": "Communications messages booked into THIS shift for the\ncaller (audience, dismissal and acknowledgement already\napplied) — at most one today; an array so the shape can\ngrow. Rendering it counts as \"shown\", as on the web.\n",
                          "items": {
                            "$ref": "#/components/schemas/CommsPlacement"
                          }
                        }
                      }
                    }
                  ]
                },
                "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 (`error.code` is `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "error": {
                    "code": "forbidden",
                    "message": "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"
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Shifts"
        ],
        "summary": "Update a shift",
        "description": "Updates the shift's own fields (wrapped in `shift`) and, when `user_ids` is sent, replaces its roster\n(`user_ids: []` clears it). `location_id` is not permitted here and is ignored. People assigned to\nthe shift are told about a material change (time, place, cancellation) after it is saved.\nRequires `write:shifts`, a manager-or-above role, the Shifts **Edit** permission, and access to this\nshift's site. Refused with 422 when the shift is managed in the workforce management system\n(`externally_managed`), or has already ended in a published schedule while the organization's\n\"edit past shifts\" setting is off (`past_shift_locked`).\n\nA roster that takes off someone working the shift is delete-level (owner decision\n2026-10-04): that request also requires `destructive:scheduling`. Adding people, or\ndropping someone who declined, needs only `write:shifts`.\n\nCancelling a shift (`shift.status` set to `cancelled`) is delete-level (owner\ndecision 2026-10-04, #6721): requires the `destructive:scheduling` scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Shift ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shift"
                ],
                "properties": {
                  "shift": {
                    "type": "object",
                    "description": "The shift's fields. On create, `name`, `start_time` and `end_time` are required. `location_id`\nis refused on create (400) and ignored on update; `needs_coverage` is computed, never sent.\nAn explicit `\"urgent\": null` is ignored (the flag is left as it was).\n",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string",
                        "nullable": true
                      },
                      "start_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "status": {
                        "type": "string",
                        "description": "Defaults to the shift's initial status when omitted"
                      },
                      "location_department_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "A department of the shift's location"
                      },
                      "required_staff_count": {
                        "type": "integer"
                      },
                      "urgent": {
                        "type": "boolean"
                      },
                      "shift_label_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "planned_breaks_attributes": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "An existing planned break to change (omit to add one)"
                            },
                            "break_type_id": {
                              "type": "integer"
                            },
                            "timing_type": {
                              "type": "string"
                            },
                            "scheduled_time": {
                              "type": "string"
                            },
                            "window_start": {
                              "type": "string"
                            },
                            "window_end": {
                              "type": "string"
                            },
                            "timing_preferences": {
                              "type": "object"
                            },
                            "is_required": {
                              "type": "boolean"
                            },
                            "_destroy": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  },
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "People to assign (members of this organization). Omit to leave the roster alone; on update,\nan empty array clears it.\n"
                  }
                }
              },
              "example": {
                "shift": {
                  "end_time": "2026-10-05T17:00:00Z",
                  "required_staff_count": 4
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shift updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "start_time",
                    "end_time",
                    "status",
                    "created_at",
                    "updated_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "status": {
                      "type": "string"
                    },
                    "location_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "location_department_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "schedule_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_sub_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "shift_type": {
                      "type": "string",
                      "nullable": true
                    },
                    "required_staff_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "needs_coverage": {
                      "type": "boolean"
                    },
                    "urgent": {
                      "type": "boolean"
                    },
                    "ends_next_day": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_open_shift": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_ad_hoc": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "location": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "first_name": {
                            "type": "string"
                          },
                          "last_name": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "planned_breaks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "break_type_id": {
                            "type": "integer"
                          },
                          "timing_type": {
                            "type": "string"
                          },
                          "scheduled_time": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_start": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_end": {
                            "type": "string",
                            "nullable": true
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "nullable": true
                          },
                          "is_required": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The `shift` object is missing",
            "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. `error.code` says which gate refused it, and `error.message` why:\n* `insufficient_permissions` — the token lacks `write:shifts`\n  (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Required scope: write:shifts\"}}`);\n* `forbidden` — the person's role, site or Shifts permission refused it:\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to manage shifts\"}}` (not a manager),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You do not have permission to manage shifts at this location\"}}` (another site),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"Not authorized to access this shift\"}}` (a shift they may not see; update and delete only), or\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to <create|edit|delete> shifts.\"}, \"status\": \"error\", \"success\": false, \"message\": \"…\"}`\n  (the Shifts Create / Edit / Delete permission).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (`errors[]`, one entry per attribute), or `externally_managed` /\n`past_shift_locked` (an `error` object).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "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"
                              },
                              "code": {
                                "type": "string",
                                "description": "Always `validation_error`"
                              },
                              "field_name": {
                                "type": "string",
                                "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "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": [
          "Shifts"
        ],
        "summary": "Update a shift (PUT)",
        "description": "Updates the shift's own fields (wrapped in `shift`) and, when `user_ids` is sent, replaces its roster\n(`user_ids: []` clears it). `location_id` is not permitted here and is ignored. People assigned to\nthe shift are told about a material change (time, place, cancellation) after it is saved.\nRequires `write:shifts`, a manager-or-above role, the Shifts **Edit** permission, and access to this\nshift's site. Refused with 422 when the shift is managed in the workforce management system\n(`externally_managed`), or has already ended in a published schedule while the organization's\n\"edit past shifts\" setting is off (`past_shift_locked`).\n\nA roster that takes off someone working the shift is delete-level (owner decision\n2026-10-04): that request also requires `destructive:scheduling`. Adding people, or\ndropping someone who declined, needs only `write:shifts`.\n\nCancelling a shift (`shift.status` set to `cancelled`) is delete-level (owner\ndecision 2026-10-04, #6721): requires the `destructive:scheduling` scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Shift ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shift"
                ],
                "properties": {
                  "shift": {
                    "type": "object",
                    "description": "The shift's fields. On create, `name`, `start_time` and `end_time` are required. `location_id`\nis refused on create (400) and ignored on update; `needs_coverage` is computed, never sent.\nAn explicit `\"urgent\": null` is ignored (the flag is left as it was).\n",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string",
                        "nullable": true
                      },
                      "start_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "end_time": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "status": {
                        "type": "string",
                        "description": "Defaults to the shift's initial status when omitted"
                      },
                      "location_department_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "A department of the shift's location"
                      },
                      "required_staff_count": {
                        "type": "integer"
                      },
                      "urgent": {
                        "type": "boolean"
                      },
                      "shift_label_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "notes": {
                        "type": "string",
                        "nullable": true
                      },
                      "planned_breaks_attributes": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "An existing planned break to change (omit to add one)"
                            },
                            "break_type_id": {
                              "type": "integer"
                            },
                            "timing_type": {
                              "type": "string"
                            },
                            "scheduled_time": {
                              "type": "string"
                            },
                            "window_start": {
                              "type": "string"
                            },
                            "window_end": {
                              "type": "string"
                            },
                            "timing_preferences": {
                              "type": "object"
                            },
                            "is_required": {
                              "type": "boolean"
                            },
                            "_destroy": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  },
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "People to assign (members of this organization). Omit to leave the roster alone; on update,\nan empty array clears it.\n"
                  }
                }
              },
              "example": {
                "shift": {
                  "end_time": "2026-10-05T17:00:00Z",
                  "required_staff_count": 4
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shift updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "start_time",
                    "end_time",
                    "status",
                    "created_at",
                    "updated_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "start_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "status": {
                      "type": "string"
                    },
                    "location_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "location_department_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "schedule_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scheduling_sub_group_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "shift_type": {
                      "type": "string",
                      "nullable": true
                    },
                    "required_staff_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "needs_coverage": {
                      "type": "boolean"
                    },
                    "urgent": {
                      "type": "boolean"
                    },
                    "ends_next_day": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_open_shift": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "is_ad_hoc": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "location": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "first_name": {
                            "type": "string"
                          },
                          "last_name": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "job_title": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "planned_breaks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "break_type_id": {
                            "type": "integer"
                          },
                          "timing_type": {
                            "type": "string"
                          },
                          "scheduled_time": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_start": {
                            "type": "string",
                            "nullable": true
                          },
                          "window_end": {
                            "type": "string",
                            "nullable": true
                          },
                          "duration_minutes": {
                            "type": "integer",
                            "nullable": true
                          },
                          "is_required": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The `shift` object is missing",
            "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. `error.code` says which gate refused it, and `error.message` why:\n* `insufficient_permissions` — the token lacks `write:shifts`\n  (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Required scope: write:shifts\"}}`);\n* `forbidden` — the person's role, site or Shifts permission refused it:\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to manage shifts\"}}` (not a manager),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You do not have permission to manage shifts at this location\"}}` (another site),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"Not authorized to access this shift\"}}` (a shift they may not see; update and delete only), or\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to <create|edit|delete> shifts.\"}, \"status\": \"error\", \"success\": false, \"message\": \"…\"}`\n  (the Shifts Create / Edit / Delete permission).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (`errors[]`, one entry per attribute), or `externally_managed` /\n`past_shift_locked` (an `error` object).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "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"
                              },
                              "code": {
                                "type": "string",
                                "description": "Always `validation_error`"
                              },
                              "field_name": {
                                "type": "string",
                                "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "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": [
          "Shifts"
        ],
        "summary": "Delete a shift",
        "description": "Deletes the shift; people actively assigned to it are told. Refused while the shift has attendance\nrecords. Requires `write:shifts`, a manager-or-above role, the Shifts **Delete** permission and access\nto this shift's site. Refused with 422 when the shift is managed in the workforce management system\n(`externally_managed`) or has already ended in a published schedule while \"edit past shifts\" is off\n(`past_shift_locked`).\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:scheduling`\nscope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Shift ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Shift deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused. `error.code` says which gate refused it, and `error.message` why:\n* `insufficient_permissions` — the token lacks `write:shifts`\n  (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Required scope: write:shifts\"}}`);\n* `forbidden` — the person's role, site or Shifts permission refused it:\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to manage shifts\"}}` (not a manager),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You do not have permission to manage shifts at this location\"}}` (another site),\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"Not authorized to access this shift\"}}` (a shift they may not see; update and delete only), or\n  `{\"error\": {\"code\": \"forbidden\", \"message\": \"You don't have permission to <create|edit|delete> shifts.\"}, \"status\": \"error\", \"success\": false, \"message\": \"…\"}`\n  (the Shifts Create / Edit / Delete permission).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The shift has attendance records\n(`{\"error\": {\"code\": \"validation_failed\", \"message\": \"Cannot delete shift with associated attendance records\"}}`),\nor `error.code` `externally_managed` / `past_shift_locked`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "required": [
                        "code",
                        "message"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shifts/{id}/claim": {
      "post": {
        "tags": [
          "Shifts"
        ],
        "summary": "Claim an open shift",
        "description": "Employee claims an available open shift. Where the company's claim\napproval setting approves the claim at once, the shift is assigned and\nthe response carries `assignment`. Where the claim needs approval\n(as Smart Approval decides claim by claim, or always under \"Require\nManager Approval for All Claims\"), a pending claim request is filed\ninstead and the response\ncarries `claim_request`; the shift is the claimant's only once an\napprover approves it. Both answer 200.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The claim was taken. `assignment` when the shift was assigned at\nonce; `claim_request` (status `pending`) when it waits for approval.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Shift claimed successfully"
                    },
                    "shift": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 123
                        },
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record that owns this shift (\"Managed in UKG\"), or null. A managed shift is read-only here: coverage requests and edits answer 422 `externally_managed`."
                        },
                        "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"
                            },
                            "lister_status": {
                              "type": "string",
                              "enum": [
                                "open",
                                "filled",
                                "traded",
                                "closed",
                                "cancelled"
                              ],
                              "description": "Present only when the listing is the caller's own. The status as its lister reads it: listing_status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                              "example": "traded"
                            },
                            "lister_status_label": {
                              "type": "string",
                              "description": "Present only when the listing is the caller's own. The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                              "example": "Traded"
                            },
                            "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",
                      "description": "Present when the shift was assigned at once.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "shift_id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "example": "assigned"
                        },
                        "assignment_type": {
                          "type": "string"
                        },
                        "scheduled_hours": {
                          "type": "number"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "claim_request": {
                      "type": "object",
                      "description": "Present when the claim waits for an approver.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "What the assignment warned about, when it landed with warnings.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The shift cannot be claimed. `error.code` says why:\n`already_assigned` (already on the shift, or declined or taken off it),\n`already_requested` (a request for it already waits for approval),\n`shift_not_available` (not open for claiming, or its schedule is not\npublished), `not_eligible` (the claimant does not meet the shift's\neligibility rules, such as its location, team or role),\nor `shift_conflict` (it overlaps one of the claimant's shifts).\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": "The claim was refused when it was written (`claim_failed`) — for\nexample a staffing rule such as an hour limit or a required\ncertification. Answered with the record's validation errors, or an\n`Error` with code `claim_failed`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "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"
                              },
                              "code": {
                                "type": "string",
                                "description": "Always `validation_error`"
                              },
                              "field_name": {
                                "type": "string",
                                "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "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: Close your open coverage listing for this shift\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\nScopes: every action needs `write:shifts`. `cancel_request` closes your\nlisting, which is delete-level (owner decision 2026-10-04): it also needs\n`destructive:shift_marketplace`.\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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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 withdrawn"
                    }
                  }
                }
              }
            }
          },
          "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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\nSubmitting twice files once. A second submission whose hours overlap a\nrequest the caller already has pending — or, for a direct entry, a day\nthe caller already added — is refused with 422 `validation_failed`.\nSend ONE `Idempotency-Key` per form (mint it when the form opens, not on\neach tap): a repeat carrying the same key replays the first 201\n(`X-Idempotency-Cached: true`) instead of answering 422, so a double\ntap still lands on the success screen.\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": "Created. For a caller whose role carries the Time & Attendance grant for their own records (`direct_entry: true`) the day is ADDED to their timesheet outright — reviewed on creation, no review queue, nobody notified; for everyone else it is a request pending manager review. Read `GET /missing_punch_requests/window` first: its `direct_entry` says which of the two this caller gets, so the form can promise the right thing.\n",
            "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"
                              }
                            }
                          }
                        }
                      }
                    },
                    "direct_entry": {
                      "type": "boolean",
                      "description": "TRUE when the caller's role carries the Time & Attendance grant for their own records: the record was written reviewed and is already on their timesheet, and `managers_notified` is false because there was nothing to review. Word the confirmation as \"added to your timesheet\", not \"sent to your manager\".\n"
                    },
                    "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",
                        "not_needed"
                      ],
                      "description": "Why `managers_notified` is what it is (`not_needed` when `direct_entry` is true — nobody reviews a day the caller added under their own grant). 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, hours already pending or already added) 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 retroactive correction window that applies to THIS caller, in days — the tenant setting for a request, 0 when `direct_entry` is true (a grant holder is bounded only by the pay period and \"not in the future\").\n"
                        },
                        "direct_entry": {
                          "type": "boolean",
                          "description": "TRUE when submitting will ADD the day to the caller's timesheet outright (their role carries Create or Manage on Time & Attendance, the \"Own data\" half of the grant); FALSE when it FILES a request a manager reviews. Switch the form's copy on it.\n"
                        },
                        "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:attendance` scope.\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"
                }
              }
            }
          }
        }
      }
    },
    "/break_attestations/{id}/answer": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Answer a rest-break question put off to this clock-in",
        "description": "Records the caller's own answer to a rest-break question that was not asked\nwhen their earlier shift closed (a manager, the shared kiosk or the automatic\nclock-out closed it). The questions that wait are the\n`rest_break_confirmations` of `POST /attendance_records/{id}/check_in`,\nthe verified `POST /attendance_records` and\n`GET /attendance_records/status`. The caller\nmust be clocked in. The question of a punch that is still open is answered\nwith its own clock-out (`break_attestation` on `…/check_out`), not here.\nNeeds `write:attendance` (or the employee's own `write:own_attendance`).\nA replay of an answer already on file answers 200 with `changed: false`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "break_attestation"
                ],
                "properties": {
                  "break_attestation": {
                    "type": "object",
                    "properties": {
                      "all_rest_breaks_taken": {
                        "type": "string",
                        "enum": [
                          "yes",
                          "no"
                        ]
                      },
                      "missed_or_short_count": {
                        "type": "string",
                        "description": "How many rest breaks were missed or short, when `all_rest_breaks_taken` is `no`.",
                        "example": "2"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded (or already on file — `changed` is false then).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changed": {
                      "type": "boolean"
                    },
                    "break_attestation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "confirmed",
                            "disputed",
                            "pending",
                            "deferred"
                          ]
                        },
                        "missed_break_count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "outcome": {
                          "type": "string",
                          "example": "All rest breaks taken"
                        }
                      }
                    },
                    "rest_break_confirmations": {
                      "type": "array",
                      "description": "What still waits for the caller after this answer.",
                      "items": {
                        "$ref": "#/components/schemas/RestBreakConfirmation"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `not_your_record` — the question is someone else's;\n`own_act_only` — a service account acting as the person, or an admin's\nView-as, cannot make this certification; or `insufficient_permissions`\n— the token lacks `write:attendance`. Time & Attendance switched off for\nthe organization, or not open to the caller, also answers 403, as every\nattendance endpoint does.\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": "`error.code` = `not_found` — no such question 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": "`error.code` = `not_waiting` — the question is not waiting to be asked\n(not outstanding, the shift's punch is still open, or it is past the\n14-day window); `not_clocked_in` — clock in first; `answer_required` —\nno answer chosen; `invalid_answer` — the count is out of range.\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": {
      "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "managed_by": {
                                "type": "string",
                                "nullable": true,
                                "example": "UKG",
                                "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                              },
                              "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
                              },
                              "review_reason_label": {
                                "type": "string",
                                "nullable": true,
                                "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                              },
                              "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": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      },
      "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\n**Your own punch is always a verified clock-in at the current time.**\nWhen the record is for the caller (`user_id` omitted or the caller's\nown) and the request is made with the person's own sign-in — not a\nservice-account token or an impersonation minted by one — the punch\nalways runs the verification pipeline (photo, geofence, IP, device,\ntime window, review flagging) as `verify: true` does, and is stamped\nwith the server's time: a `check_in_time` sent with it is ignored, and\nan ad-hoc shift starts now. A `check_out_time` is refused (403), and a\n`shift_id` must be a shift the caller is assigned to (422\n`shift_not_held`). Time already worked, or a punch that was missed, is\nreported through `POST /missing_punch_requests`, which a manager\nreviews.\n\nA manager recording a punch for someone they manage, and a service\naccount, may send `check_in_time` (required unless `verify: true`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "ID of the user checking in. Omit, or send your own id, to clock yourself 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. On your own punch it must be a shift you are assigned to."
                  },
                  "check_in_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Time of check-in. Required when a manager or a service account records someone's punch without `verify: true`. Ignored on your own punch, which is stamped with the server's time."
                  },
                  "verify": {
                    "type": "boolean",
                    "description": "Run the verification pipeline and stamp the current time. Always on for your own punch."
                  },
                  "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. The record is wrapped in\n`attendance_record`, the same envelope GET /attendance_records/{id}\nanswers with; a `verify: true` punch also carries `notification_sent`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attendance_record"
                  ],
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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"
                          }
                        }
                      }
                    },
                    "notification_sent": {
                      "type": "boolean",
                      "description": "Present on a `verify: true` punch only."
                    }
                  }
                }
              }
            }
          },
          "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. `error.code` =\n`shift_not_held` — your own punch named a shift you are not\nassigned to; report time on another shift through\n`POST /missing_punch_requests`.\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 `check_out_time` was sent (`insufficient_permissions`:\nyour own punch never arrives closed, and someone else's needs a\nmanager), 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"
                  },
                  "close_at_shift_end": {
                    "type": "boolean",
                    "description": "Record the clock-out at the shift's end instead of now, flagged for the\napprover to review. Offered when clocking was paused (a freeze, lockdown,\nsuspension or maintenance) at the shift's end and has since resumed —\n`GET /attendance_records/status` names that time in\n`shift_end_clock_out_at` — and for a punch forgotten past its shift.\nWithout it the clock-out is recorded when it is sent.\n"
                  }
                }
              }
            }
          }
        },
        "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": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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. Changing a\ncheck_in_time or check_out_time requires `edit_reason` (422\n`edit_reason_required` without it); it is recorded in the edit history.\n",
        "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"
                  },
                  "edit_reason": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Why the punch time changed. Required when a check_in_time or check_out_time changes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attendance record updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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"
                          }
                        }
                      }
                    },
                    "timesheet_warning": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when the new times were saved on the attendance record but NOT applied to the employee's timesheet, because that timesheet is already approved — the sentence says what to do (return it for correction, or, for your own punch, use Request Correction). Null when the timesheet was updated or there is none.",
                      "example": "Their approved timesheet was not changed — return it for correction to apply the new times."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Attendance"
        ],
        "summary": "Delete attendance record",
        "description": "Permanently deletes an attendance record. Admin-only in most environments.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:attendance` scope.\n\nWhen the punch sat on the employee's APPROVED timesheet, the record is\ndeleted but the timesheet keeps its hours (an approved wage record is\nnever rewritten); the answer is then 200 with `timesheet_warning`\ninstead of 204. When someone other than the employee deletes it, the\nemployee gets an in-app notice.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attendance record deleted, but the employee's approved timesheet still has its hours.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timesheet_warning": {
                      "type": "string",
                      "description": "What to do about the hours the approved timesheet kept (return it for correction, or, for your own punch, use Request Correction).",
                      "example": "Their approved timesheet still has the deleted punch's hours — return it for correction to remove them."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The caller's unread notification count, carried on every JSON answer (null without the read:notifications scope)."
                    }
                  }
                }
              }
            }
          },
          "204": {
            "description": "Attendance record deleted successfully, and no approved timesheet kept its hours"
          }
        }
      }
    },
    "/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": {
                    "managed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "UKG",
                      "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                    },
                    "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
                    },
                    "review_reason_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                    },
                    "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": {
                    "managed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "UKG",
                      "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                    },
                    "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
                    },
                    "review_reason_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                    },
                    "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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:attendance` scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Record rejected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "managed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "UKG",
                      "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                    },
                    "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
                    },
                    "review_reason_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                    },
                    "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.\n`notes` is the reason for the change and is required (422 when blank); it is\nrecorded in the edit history and shown to the employee.\n",
        "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": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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"
                          }
                        }
                      }
                    },
                    "notification_sent": {
                      "type": "boolean",
                      "description": "Whether the employee was told about the adjustment"
                    },
                    "timesheet_warning": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when the new times were saved on the attendance record but NOT applied to the employee's timesheet, because that timesheet is already approved — the sentence says what to do (return it for correction, or, for your own punch, use Request Correction). Null when the timesheet was updated or there is none.",
                      "example": "Their approved timesheet was not changed — return it for correction to apply the new times."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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. `clock_in_brief_path` is where to fetch\n\"Before you start\" for this punch next —\n`GET /attendance_records/{id}/clock_in_brief`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attendance_record": {
                      "type": "object",
                      "properties": {
                        "managed_by": {
                          "type": "string",
                          "nullable": true,
                          "example": "UKG",
                          "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                        },
                        "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
                        },
                        "review_reason_label": {
                          "type": "string",
                          "nullable": true,
                          "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                        },
                        "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"
                          }
                        }
                      }
                    },
                    "notification_sent": {
                      "type": "boolean"
                    },
                    "clock_in_brief_path": {
                      "type": "string",
                      "example": "/api/v1/attendance_records/123/clock_in_brief"
                    },
                    "rest_break_confirmations": {
                      "type": "array",
                      "description": "Rest-break questions put off to this clock-in (a manager, the\nshared kiosk or the automatic clock-out closed an earlier\nshift without asking). Empty unless the business enforces\nrest-break confirmation. Ask each one and post the answer to\n`POST /break_attestations/{id}/answer`.\n",
                      "items": {
                        "$ref": "#/components/schemas/RestBreakConfirmation"
                      }
                    }
                  }
                }
              }
            }
          },
          "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": {
                    "managed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "UKG",
                      "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                    },
                    "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
                    },
                    "review_reason_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                    },
                    "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"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "end_at_planned": {
                    "type": "boolean",
                    "description": "Record the break's end at its planned end, flagged for review. Honoured only while the status answer offers `planned_break_end_at`; otherwise the break ends now."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Break ended successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "managed_by": {
                      "type": "string",
                      "nullable": true,
                      "example": "UKG",
                      "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                    },
                    "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
                    },
                    "review_reason_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                    },
                    "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/{id}/clock_in_brief": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Before you start — the clock-in brief",
        "description": "The one briefing at clock-in (Knowledge Layer E1) for the caller's OWN\nopen punch, \"since your last clock-out\": what they still owe a\nconfirmation on (the same set as `waiting_on_you` on `GET /home`), what\nchanged on their schedule, the work waiting and what arrived meanwhile.\nThe same brief the web and phone Time & Attendance pages show after a\npunch. Call it right after `POST /attendance_records/{id}/check_in`\n(whose response carries `clock_in_brief_path`), and again after a\nconfirm or a relaunch. Nothing is stored: fetching it never changes the\ncaller's Home \"Catch me up\". `waiting_on_you`, `to_decide` and `owed` are\nthe Inbox's, so they are `null` for a token without `read:notifications`\n(the native app's sign-in tokens carry it).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The brief.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "clock_in_brief": {
                      "$ref": "#/components/schemas/ClockInBrief"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `not_your_record` — the punch belongs to someone else.",
            "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` = `not_clocked_in` — the punch is not open.",
            "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}/clock_in_brief/confirm": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Confirm a must-read from the clock-in brief",
        "description": "Confirms one owed item from the brief in place, for the caller's own open\npunch. Only items with `confirm_in_chat: true` — feed must-read posts and\nbroadcasts; every other kind is confirmed on its own page (its `url`).\nNeeds `write:attendance` and `write:notifications` (an acknowledgment of\nsomething the Inbox says is owed); a broadcast also needs\n`write:broadcasts`. Answers with the brief as it now stands.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "item_id"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "description": "The item's `kind` (`must_read_post` or `broadcast`).",
                    "example": "must_read_post"
                  },
                  "item_id": {
                    "type": "integer",
                    "description": "The item's `id`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed (or already confirmed — `changed` is false then).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Acknowledged."
                    },
                    "changed": {
                      "type": "boolean"
                    },
                    "clock_in_brief": {
                      "$ref": "#/components/schemas/ClockInBrief"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`error.code` = `not_your_record`, or `insufficient_permissions` when a\nbroadcast is confirmed with a token lacking `write:broadcasts`.\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` = `not_clocked_in`; `confirm_on_page` — this kind is\nconfirmed on its own page; `not_pending` — the item isn't waiting on\nthe caller; or the kind's own refusal (`closed`, `own_post`, …).\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/status": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get the caller's live clock status",
        "description": "Whether the authenticated user is on the clock right now, since when, where, and whether a\nbreak is running. Works with an employee's own-attendance token (`read:own_attendance`).\nAlso returns the rest-break questions put off to this clock-in and the undo-clock-out window\n(the same `undo` object `POST /attendance_records/{id}/check_out` returns). Alias:\n`GET /attendance/status`.\n",
        "responses": {
          "200": {
            "description": "Live clock status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "clocked_in": {
                      "type": "boolean"
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Clock-in time (present only while clocked in)"
                    },
                    "work_mode": {
                      "type": "string",
                      "description": "on_site, remote or off_site (present only while clocked in)"
                    },
                    "off_site_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "on_break": {
                      "type": "boolean",
                      "description": "Any running break, paid or unpaid (present only while clocked in)"
                    },
                    "location": {
                      "type": "object",
                      "nullable": true
                    },
                    "attendance_record_id": {
                      "type": "integer"
                    },
                    "rest_break_confirmations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "punches_paused": {
                      "type": "boolean",
                      "description": "True while the tenant's maintenance refuses punches for this caller\n(the answer `GET /home` gives as `attendance_policy.punches_paused`):\ndraw Clock In / Clock Out and breaks disabled. While true,\n`shift_end_clock_out_at` and `planned_break_end_at` are null.\n"
                    },
                    "shift_end_clock_out_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "The shift's end, when clocking was paused then and has since resumed:\noffer \"Clock out at <this time>\" beside Clock Out, and send\n`close_at_shift_end: true` with `POST /attendance_records/{id}/check_out`\nto record it (flagged for review). Null otherwise, and while punches are paused.\n"
                    },
                    "planned_break_end_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "The running break's planned end, when clocking was paused then and has\nsince resumed: offer \"End break at <this time>\" beside End Break, and send\n`end_at_planned: true` with `POST /attendance_records/{id}/end_break` to\nrecord it (flagged for review). Null otherwise, and while punches are paused.\n"
                    },
                    "undo": {
                      "type": "object",
                      "description": "The undo-clock-out window; `undoable` is false while a punch is open"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "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
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/currently_clocked_in": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "List who is on the clock right now",
        "description": "The live roster (\"who is on site now\") — the emergency-mustering feed. One row per person, their\nmost recent open punch, longest on shift first. Scoped to the people the caller may see (an\nadmin, everyone; a manager, their locations and reports). Requires `read:attendance`; an\nown-attendance token is refused. A coworker's email is included only where the People\ndirectory shows email to the caller. An unknown `work_mode` value is dropped (and named in\n`meta`) rather than returning an empty roster. Alias: `GET /attendance/currently_clocked_in`.\n",
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Only punches at this location or beneath it"
          },
          {
            "name": "work_mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated work modes (on_site, remote, off_site)"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Live roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currently_clocked_in": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "first_name": {
                                "type": "string"
                              },
                              "last_name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "since": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "work_mode": {
                            "type": "string"
                          },
                          "off_site_reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "location": {
                            "type": "object",
                            "nullable": true
                          },
                          "attendance_record_id": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "counts_by_work_mode": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "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
                    }
                  }
                }
              }
            }
          },
          "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
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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": {
                      "managed_by": {
                        "type": "string",
                        "nullable": true,
                        "example": "UKG",
                        "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                      },
                      "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
                      },
                      "review_reason_label": {
                        "type": "string",
                        "nullable": true,
                        "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                      },
                      "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 still open (no check-out) more than 24 hours after clock-in. An employee sees only their own; an administrator or manager sees the records they may read.\n\nReturns at most 100 rows, newest clock-in first. The cap is disclosed, not\nsilent: `total_count` is the true number of matching records and\n`truncated` is true when more exist than were returned. A client showing\nthis list should say so (e.g. \"Showing 100 of N\") rather than present the\nfirst 100 as the whole set.\n",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Only this person's records, within what the caller may read (it narrows, never widens;\nan id outside the caller's scope returns none). Applied before the count and the cap, so\n`total_count` is that person's own total. A first-person view (the caller's own punches)\npasses the caller's id: an administrator's unfiltered list is the whole tenant, and the\ncaller's own rows could fall past its first 100.\n",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Records missing check-out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "missing_check_out",
                    "total_count",
                    "returned_count",
                    "truncated"
                  ],
                  "properties": {
                    "missing_check_out": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "managed_by": {
                            "type": "string",
                            "nullable": true,
                            "example": "UKG",
                            "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                          },
                          "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
                          },
                          "review_reason_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                          },
                          "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"
                            }
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Every matching record, not only the rows returned."
                    },
                    "returned_count": {
                      "type": "integer",
                      "description": "Rows in this response (at most 100)."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when total_count exceeds the 100 rows returned."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance_records/requires_review": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Get records requiring review",
        "description": "Lists attendance records flagged for review by automated or manual checks — the review queue the caller may decide.\n\nReturns at most 100 rows, newest clock-in first. The cap is disclosed, not\nsilent: `total_count` is the true number of matching records and\n`truncated` is true when more exist than were returned. A client showing\nthis list should say so (e.g. \"Showing 100 of N\") rather than present the\nfirst 100 as the whole set.\n",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Only this person's records, within what the caller may read (it narrows, never widens;\nan id outside the caller's scope returns none). Applied before the count and the cap, so\n`total_count` is that person's own total. A first-person view (the caller's own punches)\npasses the caller's id: an administrator's unfiltered list is the whole tenant, and the\ncaller's own rows could fall past its first 100.\n",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Records requiring review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "requires_review",
                    "total_count",
                    "returned_count",
                    "truncated"
                  ],
                  "properties": {
                    "requires_review": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "managed_by": {
                            "type": "string",
                            "nullable": true,
                            "example": "UKG",
                            "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
                          },
                          "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
                          },
                          "review_reason_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
                          },
                          "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"
                            }
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Every matching record, not only the rows returned."
                    },
                    "returned_count": {
                      "type": "integer",
                      "description": "Rows in this response (at most 100)."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when total_count exceeds the 100 rows returned."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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/summary": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Menu counts",
        "description": "The numbers the Shift Marketplace menu shows beside its items, for a client\nthat draws its own menu. They are the same counts the desktop menu and the\nphone web page show for the same person (one counter serves all three),\ncounted over the whole set, not one page.\n\n- `pickable_now` — the Listings item and the phone's Browse button: shifts\n  the caller can pick up or trade for right now. Leaves out the caller's own\n  listings, shifts they already asked for or applied to trade for, and\n  pickups the claim rules (rest period, hours, leave) would refuse.\n- `claims_waiting_on_you` — the Shift Claims item: pending shift claims the\n  caller may approve or reject. Always 0 when the menu offers the caller no\n  Shift Claims item.\n- `can_decide_claims` — whether the menu offers the caller a Shift Claims\n  item at all (they are named by the tenant's claim approval authority, or\n  they administer the app and may manage shifts) AND the token carries\n  `write:shifts`, which approve and reject require.\n\nA menu draws no number at zero; this endpoint answers 0.\n\n**Scope**: `read:shifts`.\n**Business Scoping**: Counts are scoped to the current business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The menu's counts for the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pickable_now",
                    "claims_waiting_on_you",
                    "can_decide_claims"
                  ],
                  "properties": {
                    "pickable_now": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Shifts the caller can pick up or trade for right now.",
                      "example": 3
                    },
                    "claims_waiting_on_you": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Pending shift claims waiting on the caller's approval (0 when the caller has no Shift Claims item).",
                      "example": 2
                    },
                    "can_decide_claims": {
                      "type": "boolean",
                      "description": "Whether the caller has a Shift Claims queue to decide from and the token may decide (write:shifts).",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Shift Marketplace is off for the business, the caller cannot see it, or the token lacks read:shifts."
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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 (false while their own request on it waits)"
                              },
                              "pending_request": {
                                "type": "object",
                                "nullable": true,
                                "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "requested_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "nullable": true
                                  }
                                }
                              },
                              "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"
                              },
                              "lister_status": {
                                "type": "string",
                                "enum": [
                                  "open",
                                  "filled",
                                  "traded",
                                  "closed",
                                  "cancelled"
                                ],
                                "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                                "example": "traded"
                              },
                              "lister_status_label": {
                                "type": "string",
                                "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                                "example": "Traded"
                              },
                              "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": {
                          "description": "The four pagination values plus Kaminari's page numbers (this namespace's own pagination meta: no `has_next_page` / `has_prev_page` flags).",
                          "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": {
                                "next_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The next page number; null on the last page."
                                },
                                "prev_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The previous page number; null on the first page."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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 (false while their own request on it waits)"
                        },
                        "pending_request": {
                          "type": "object",
                          "nullable": true,
                          "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "requested_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            }
                          }
                        },
                        "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"
                        },
                        "lister_status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "traded",
                            "closed",
                            "cancelled"
                          ],
                          "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                          "example": "traded"
                        },
                        "lister_status_label": {
                          "type": "string",
                          "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                          "example": "Traded"
                        },
                        "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 (false while their own request on it waits)"
                        },
                        "pending_request": {
                          "type": "object",
                          "nullable": true,
                          "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "requested_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            }
                          }
                        },
                        "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"
                        },
                        "lister_status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "traded",
                            "closed",
                            "cancelled"
                          ],
                          "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                          "example": "traded"
                        },
                        "lister_status_label": {
                          "type": "string",
                          "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                          "example": "Traded"
                        },
                        "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 (false while their own request on it waits)"
                        },
                        "pending_request": {
                          "type": "object",
                          "nullable": true,
                          "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "requested_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            }
                          }
                        },
                        "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"
                        },
                        "lister_status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "traded",
                            "closed",
                            "cancelled"
                          ],
                          "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                          "example": "traded"
                        },
                        "lister_status_label": {
                          "type": "string",
                          "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                          "example": "Traded"
                        },
                        "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)\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:shift_marketplace`\nscope.\n",
        "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 (false while their own request on it waits)"
                        },
                        "pending_request": {
                          "type": "object",
                          "nullable": true,
                          "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "requested_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            }
                          }
                        },
                        "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"
                        },
                        "lister_status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "filled",
                            "traded",
                            "closed",
                            "cancelled"
                          ],
                          "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                          "example": "traded"
                        },
                        "lister_status_label": {
                          "type": "string",
                          "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                          "example": "Traded"
                        },
                        "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/listings/{id}/withdraw_claim": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Withdraw My Pickup Request",
        "description": "Withdraw the caller's own pending pickup request for this listing's shift.\nA listing's `pending_request` names the request; `can_claim` is false while one waits.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Request withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "claim_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "example": "withdrawn"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No pending request of the caller's for 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"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "The request could not be withdrawn",
            "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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"
                              },
                              "status_label": {
                                "type": "string",
                                "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                                "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": {
                          "description": "The four pagination values plus Kaminari's page numbers (this namespace's own pagination meta: no `has_next_page` / `has_prev_page` flags).",
                          "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": {
                                "next_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The next page number; null on the last page."
                                },
                                "prev_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The previous page number; null on the first page."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Apply to Listing",
        "description": "Offer one of your own upcoming shifts in exchange for a Trade Only or Pickup or Trade listing. An application must offer a shift; taking the shift without a trade is a pickup (claim the listing), which goes through the company's approval rule.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "application"
                ],
                "properties": {
                  "application": {
                    "type": "object",
                    "required": [
                      "listing_id",
                      "offered_shift_id"
                    ],
                    "properties": {
                      "listing_id": {
                        "type": "integer",
                        "example": 123
                      },
                      "offered_shift_id": {
                        "type": "integer",
                        "description": "One of your own upcoming shifts, given in exchange.",
                        "example": 456
                      },
                      "notes": {
                        "type": "string",
                        "example": "I can take your Thursday morning if you take my Sunday afternoon."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "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"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                          "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, wrong type, or no shift offered in exchange)",
            "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"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                          "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"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                          "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)\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:shift_marketplace` scope.\n",
        "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"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                          "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)\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:shift_marketplace`\nscope.\n",
        "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"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
                          "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/claim_requests": {
      "get": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "List the pickups waiting on you",
        "description": "The pickup requests (shift claims) waiting on the caller to approve or\ndecline — the Shift Marketplace Dashboard's \"Waiting on you\" and the Shift\nClaims queue's rows. They are the same claims the web lists for the same\nperson, oldest first, and `meta.total_count` is the number the Shift Claims\nmenu item and `GET /shift_marketplace/summary`'s `claims_waiting_on_you`\nshow.\n\nA claim is either on a coworker's listed shift (`kind: listing` — approving\nmoves the shift from `listed_by` to `user`) or on a management-posted open\nshift (`kind: open_shift`).\n\n`can_approve` is false while approving would be refused — the shift has\nstarted or been taken, its schedule is back in draft, the employee no\nlonger qualifies, or a staffing rule (hours, rest, certifications) would\nbreak — and `cannot_approve_reason` says which. Both verdicts need a token\nholding `write:shifts`; a decline also needs `destructive:scheduling`\n(declining is a delete-level decision on a token, as on\n`POST /home/waiting_on_you/decide`).\n\n**Scope**: `read:shifts`.\n**Business Scoping**: Claims are scoped to the current business.\n",
        "parameters": [
          {
            "name": "waiting_on",
            "in": "query",
            "description": "Whose decision the claims wait on. `me` is the only queue (and the default).",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ],
              "default": "me"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The claims waiting on the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 913
                          },
                          "status": {
                            "type": "string",
                            "example": "pending"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "listing",
                              "open_shift"
                            ]
                          },
                          "requested_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "shift": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "example": "Monday Floor"
                              },
                              "start_time": {
                                "type": "string",
                                "format": "date-time",
                                "description": "In the shift's own time zone."
                              },
                              "end_time": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "location": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          },
                          "user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "listed_by": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true
                              }
                            },
                            "nullable": true,
                            "description": "Whose listed shift this is (kind listing); null for an open shift."
                          },
                          "can_approve": {
                            "type": "boolean"
                          },
                          "can_reject": {
                            "type": "boolean"
                          },
                          "cannot_approve_reason": {
                            "type": "string",
                            "nullable": true,
                            "example": "This shift has already started, so the claim can no longer be approved."
                          },
                          "reject_needs_reason": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "example": 2
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        },
                        "next_page": {
                          "type": "integer",
                          "nullable": true
                        },
                        "prev_page": {
                          "type": "integer",
                          "nullable": true
                        },
                        "degraded": {
                          "type": "boolean",
                          "description": "The queue could not be read; an empty list is then not \"nothing waiting\".",
                          "example": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_an_approver` — the caller decides no shift claims in this\norganization (they neither manage shifts nor are named by its claim\napproval authority); or Shift Marketplace is off for the business, the\ncaller cannot see it, or the token lacks read:shifts.\n"
          }
        }
      }
    },
    "/shift_marketplace/claim_requests/{id}/approve": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Approve a pickup waiting on you",
        "description": "Approves the claim exactly as the Shift Claims page does: the shift is\nassigned to the employee (and taken from the lister for a listing), the\nemployee is told, and the approvers' Inbox asks for it close.\n\n**Scope**: `write:shifts`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The claim request's id (an `items[].id` from the list)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The decision landed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "The shift request has been approved."
                    },
                    "claim_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 913
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "approved",
                            "rejected"
                          ],
                          "example": "approved"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_an_approver` — the caller decides no shift claims; or the token lacks write:shifts."
          },
          "409": {
            "description": "`not_waiting` — the claim was decided already, or it is not waiting on the caller."
          },
          "422": {
            "description": "`decision_refused` — approving was refused (the shift started or was taken, a staffing rule would break); the message says why."
          }
        }
      }
    },
    "/shift_marketplace/claim_requests/{id}/reject": {
      "post": {
        "tags": [
          "Shift Marketplace"
        ],
        "summary": "Decline a pickup waiting on you",
        "description": "Declines the claim exactly as the Shift Claims page does: the employee is\ntold, with the reason when one is given, and the approvers' Inbox asks for\nit close.\n\n**Scope**: `write:shifts` and `destructive:scheduling`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The claim request's id (an `items[].id` from the list)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Why — the employee is told it.",
                    "example": "Already covered"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The decision landed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "The shift request has been approved."
                    },
                    "claim_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 913
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "approved",
                            "rejected"
                          ],
                          "example": "approved"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_an_approver` — the caller decides no shift claims; `insufficient_permissions` — the token lacks write:shifts or destructive:scheduling."
          },
          "409": {
            "description": "`not_waiting` — the claim was decided already, or it is not waiting on the caller."
          },
          "422": {
            "description": "`decision_refused` — declining was refused (for example a reason over 1000 characters); the message says why."
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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"
                              },
                              "cancellation_reason": {
                                "type": "string",
                                "nullable": true,
                                "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                              },
                              "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": {
                          "description": "The four pagination values plus Kaminari's page numbers (this namespace's own pagination meta: no `has_next_page` / `has_prev_page` flags).",
                          "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": {
                                "next_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The next page number; null on the last page."
                                },
                                "prev_page": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The previous page number; null on the first page."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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"
                        },
                        "cancellation_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                        },
                        "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"
                        },
                        "cancellation_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                        },
                        "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"
                        },
                        "cancellation_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                        },
                        "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)\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:shift_marketplace`\nscope.\n",
        "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"
                        },
                        "cancellation_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                        },
                        "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\nDelete-level (owner decision 2026-10-04): requires the `destructive:shift_marketplace`\nscope.\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"
                        },
                        "cancellation_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
                        },
                        "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",
                            "description": "Premium hours, double time INCLUDED — overtime_only_hours plus double_time_hours.",
                            "example": 0.0
                          },
                          "overtime_only_hours": {
                            "type": "number",
                            "format": "float",
                            "description": "Overtime hours excluding double time (the split /pay_information reports).",
                            "example": 0.0
                          },
                          "double_time_hours": {
                            "type": "number",
                            "format": "float",
                            "description": "Double-time hours, also counted in overtime_hours.",
                            "example": 0.0
                          },
                          "submission_date": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-22T09:00:00Z"
                          },
                          "auto_submitted": {
                            "type": "boolean",
                            "description": "True while the submission that stands was made by timesheet auto-submit when the due date passed — the hours went in as recorded and the employee did not certify them.",
                            "example": false
                          },
                          "auto_submitted_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When timesheet auto-submit first submitted this sheet. Kept after a recall; a sheet is never auto-submitted twice.",
                            "example": null
                          },
                          "approval_date": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2024-01-22T14:30:00Z"
                          },
                          "rejection_reason": {
                            "type": "string",
                            "nullable": true,
                            "description": "What the approver wrote when sending the timesheet back (its rejection reason, else the manager's notes) — the text the employee is shown. Null unless the timesheet is rejected.",
                            "example": "Missing Tuesday's clock-out"
                          },
                          "returned_for_correction": {
                            "type": "boolean",
                            "description": "True when the timesheet is rejected and that rejection was a return to the employee for corrections (not an outright rejection).",
                            "example": false
                          },
                          "correction_requested_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the employee asked for a correction to this approved timesheet. Null when no request is open; returning the timesheet for correction clears it.",
                            "example": null
                          },
                          "correction_request_reason": {
                            "type": "string",
                            "nullable": true,
                            "description": "What the employee wrote when asking for the correction. Null when no request is open.",
                            "example": null
                          },
                          "can_request_correction": {
                            "type": "boolean",
                            "description": "Whether the caller may send a correction request on this timesheet\n(POST /timesheets/{id}/request_correction): their own approved\ntimesheet, not managed in an external system.\n",
                            "example": false
                          },
                          "can_answer_correction": {
                            "type": "boolean",
                            "description": "Whether the caller may answer the open correction request\n(POST /timesheets/{id}/close_correction): an approver of the employee\nwho is not the employee or one of their reports.\n",
                            "example": false
                          },
                          "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
                          },
                          "source": {
                            "$ref": "#/components/schemas/ManagedSource"
                          },
                          "entries_count": {
                            "type": "integer",
                            "description": "Number of entries in this timesheet",
                            "example": 5
                          },
                          "missing_punches_count": {
                            "type": "integer",
                            "description": "Number of entries that still need time entered (missing a clock-in or clock-out). Absences are not counted.",
                            "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\n`due_timesheet` is the sheet the employee owes now, when it is a different\none: an ended period still waiting to be submitted, or a sheet returned for\ncorrections — the sheet the phone and web My Timesheets lead with. Null when\nnothing else is due.\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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "due_timesheet": {
                      "description": "The timesheet due for submission (an ended period not yet submitted, or one returned for corrections), when it is not `timesheet`; null when nothing else is owed.",
                      "nullable": true,
                      "allOf": [
                        {
                          "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"
                                },
                                "can_return_for_correction": {
                                  "type": "boolean",
                                  "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                                  "example": false
                                },
                                "correction_request_explainer": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                                  "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                                }
                              }
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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\nAnswers the caller's own timesheet. A caller whose token also holds\n`approve:timesheets` can read a team member's timesheet when they may\napprove for that employee: the rule the Team Timesheets review page\nuses. This is how an approver reads an open correction request before\nanswering it. Any other timesheet answers 404 `timesheet_not_found`.\n\n**Token scope:** `read:timesheets` (plus `approve:timesheets` for a team\nmember's timesheet).\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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "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}/reject": {
      "post": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Reject a submitted timesheet",
        "description": "An approver rejects a timesheet in this business that is waiting for a\ndecision (`submitted` or `regional_pending`) and sends it back to the\nemployee, who is told the reason. The caller must hold the approve\npermission for timesheets over the employee (403 `forbidden`).\n\n**Token scopes:** `approve:timesheets` and `destructive:timekeeping` —\nrejecting a timesheet is a final act (owner decision 2026-10-04, #6721),\nthe scope tool_reject_timesheet asks on Ask AI and MCP. A token missing\neither is refused with 403 `insufficient_permissions` (\"Required scope:\n…\").\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",
                "required": [
                  "rejection_reason"
                ],
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "description": "Shown to the employee. Required.",
                    "example": "Missing Tuesday's clock-out"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Timesheet rejected",
            "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "rejection": {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `approve:timesheets` or `destructive:timekeeping`) or `forbidden` (\"You don't have permission to reject this timesheet\").",
            "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": "`timesheet_not_found` — no such timesheet 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_decided` — another approver decided the timesheet first; `details.current_status` is its status now.",
            "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": "`rejection_reason_required` (no reason given) or `not_rejectable` (the timesheet is not `submitted` or `regional_pending`, or is managed by an external system; `details.current_status` says which status it is in).",
            "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}/request_correction": {
      "post": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Ask for a correction to your approved timesheet",
        "description": "The employee asks for a correction to their own APPROVED timesheet,\nwhich can no longer be edited directly. The timesheet's approvers (or,\nonce they have left, whoever may approve for the employee) get an Inbox\nask and an email with the reason. They answer it by returning the\ntimesheet for correction or by closing the request with no change. A\nsecond request while one is open is added below the first.\nSame rules as the web and phone \"Request a correction\".\n\n**Token scope:** `write:timesheets`.\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",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "What needs to be corrected. The approver reads it.",
                    "example": "Tuesday's clock-out should be 6:30 PM, not 5:30 PM"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The approvers were told.",
            "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "correction_request": {
                      "type": "object",
                      "properties": {
                        "sent_to": {
                          "type": "array",
                          "description": "The approvers who were told.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "saved": {
                          "type": "boolean",
                          "description": "False when the approvers were told but the request could not be kept on the timesheet."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `write:timesheets`).",
            "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": "`timesheet_not_found` — not one of your timesheets.",
            "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_approved` (only an approved timesheet takes a request; edit this one instead), `externally_managed` (the timesheet is managed in another system, whatever its state; request the correction there — `details.managed_by` names it), `reason_required`, `nobody_told` (approvers were found but none could be reached) or `no_approver` (nobody may answer it). `details.suggestion` says what to do.",
            "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}/close_correction": {
      "post": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Close an employee's correction request with no change",
        "description": "The approver's answer to an employee's correction request when nothing\nneeds changing. The request leaves the timesheet, the approvers' Inbox\nasks close, and the employee is told, with the optional reply. When the\nhours are already with payroll the employee is told the correction is\nmade in payroll. The caller must hold the approve permission for\ntimesheets over the employee and must not be the employee or one of\ntheir reports — the rule `can_answer_correction` reports.\n\n**Token scope:** `approve:timesheets`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Timesheet ID",
            "schema": {
              "type": "integer",
              "example": 123
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reply": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Optional. Shown to the employee.",
                    "example": "Checked the clock records — 5:30 PM is right."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request is closed.",
            "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "correction_request": {
                      "type": "object",
                      "properties": {
                        "closed": {
                          "type": "boolean"
                        },
                        "employee_told": {
                          "type": "boolean"
                        },
                        "hours_in_payroll": {
                          "type": "boolean",
                          "description": "True when the employee was told the correction is made in payroll."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `approve:timesheets`) or `forbidden` (the caller may not answer this 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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`timesheet_not_found` — no such timesheet 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": "`no_open_request` — the timesheet has no open correction 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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/timesheets/{id}/return_for_correction": {
      "post": {
        "tags": [
          "Timesheets"
        ],
        "summary": "Send an approved timesheet back to the employee to correct",
        "description": "Sends an APPROVED timesheet back to its employee to correct and\nresubmit — the approver's other answer to a correction request. Clears\nthe open request and closes the approvers' asks; the employee is told\nthe reason. Refused once the timesheet's hours have been sent, or are\nbeing sent, to payroll. Same rules as Return for correction on the\nTeam Timesheets page.\n\n**Token scopes:** `approve:timesheets` and `destructive:timekeeping`, as\nfor reject.\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",
                "required": [
                  "rejection_reason"
                ],
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "description": "What needs correcting. Shown to the employee.",
                    "example": "Fix Tuesday's clock-out and resubmit"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The timesheet was returned.",
            "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"
                            },
                            "can_return_for_correction": {
                              "type": "boolean",
                              "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                              "example": false
                            },
                            "correction_request_explainer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                              "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
                            }
                          }
                        }
                      ]
                    },
                    "rejection": {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        },
                        "returned_for_correction": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks a scope), `forbidden` (no approve permission over the employee, or the caller is the employee).",
            "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": "`timesheet_not_found` — no such timesheet 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": "`rejection_reason_required`, `externally_managed` (managed in another system), or `not_returnable` (not approved, or its hours are with payroll; `details.suggestion` says which).",
            "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 past entry still needs a clock-in or clock-out entered. False for an absence (see absent).",
                            "example": false
                          },
                          "absent": {
                            "type": "boolean",
                            "description": "Whether this entry is an absence — a scheduled day with no times and no hours — which has nothing to enter.",
                            "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",
                          "description": "Premium hours, double time INCLUDED — overtime_only_hours plus double_time_hours.",
                          "example": 0.0
                        },
                        "overtime_only_hours": {
                          "type": "number",
                          "format": "float",
                          "description": "Overtime hours excluding double time.",
                          "example": 0.0
                        },
                        "double_time_hours": {
                          "type": "number",
                          "format": "float",
                          "description": "Double-time hours, also counted in overtime_hours.",
                          "example": 0.0
                        },
                        "total_entries": {
                          "type": "integer",
                          "example": 5
                        },
                        "missing_punches": {
                          "type": "integer",
                          "description": "Entries that still need time entered. Absences are not counted.",
                          "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 — the entries whose missing_punch flag is true: a past entry still missing a clock-in or clock-out that is not an absence. false returns every other entry, absences included.",
            "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 past entry still needs a clock-in or clock-out entered. False for an absence (see absent).",
                            "example": false
                          },
                          "absent": {
                            "type": "boolean",
                            "description": "Whether this entry is an absence — a scheduled day with no times and no hours — which has nothing to enter.",
                            "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"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_entry` — an entry with exactly these punches (date, start_time, end_time) is already on the timesheet, typically a retried POST sent without an `Idempotency-Key`. Nothing was created; `details.existing_entry_id` names the entry that holds them.",
            "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 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\n**Token scopes:** `write:timesheets` and `destructive:timekeeping` —\ndeleting an entry is a delete-level act (#7246).\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\nPeriods are computed from the business's pay-period settings — the same\ncalendar `/pay_periods/current` and `/pay_periods/next` return: weekly\nperiods start on the configured start day, biweekly periods every 14 days\nfrom the configured reference start date, semi-monthly periods on the\nconfigured days of the month (clamped for short months), monthly periods\non the 1st.\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). Must be a shift the timesheet's owner is assigned to or punched against.",
                        "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",
                              "nullable": true,
                              "description": "Null when the entry has no hours. Pay is at the caller's own rate on file (hourly rate, or annual salary / 2080); when no rate is on file the hours are still given and every pay and rate field is null.",
                              "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",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 37.5
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_entry` — an entry with exactly these punches (date, start_time, end_time) is already on the timesheet, typically a retried POST sent without an `Idempotency-Key`. Nothing was created; `details.existing_entry_id` names the entry that holds them.",
            "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 failed, timesheet not editable, or `shift_not_assigned` — `shift_id` names a shift the timesheet's owner holds no (non-declined) assignment on and never punched against. Omit `shift_id` to record the time without a 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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/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",
                              "nullable": true,
                              "description": "Null when the entry has no hours. Pay is at the caller's own rate on file (hourly rate, or annual salary / 2080); when no rate is on file the hours are still given and every pay and rate field is null.",
                              "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",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "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",
                              "nullable": true,
                              "description": "Null when the entry has no hours. Pay is at the caller's own rate on file (hourly rate, or annual salary / 2080); when no rate is on file the hours are still given and every pay and rate field is null.",
                              "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",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "overtime_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 0.0
                                },
                                "total_pay": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 200.0
                                },
                                "hourly_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 25.0
                                },
                                "overtime_rate": {
                                  "type": "number",
                                  "format": "float",
                                  "nullable": true,
                                  "example": 37.5
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Update failed, timesheet not editable, or `shift_not_assigned` — a changed `shift_id` names a shift the timesheet's owner is not assigned to and never punched against.",
            "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\n**Token scopes:** `write:timesheets` and `destructive:timekeeping` —\ndeleting an entry is a delete-level act (#7246).\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": "scope",
            "in": "query",
            "required": false,
            "description": "Which set to list. `mine` (default) — your requests, and ones assigned to, created by or escalated to you, plus approvals you can give.\n`assigned`, `team`, `available` — your working set as an agent (Service Desk staff only; 403 otherwise).\n`approvals` — waiting for an approval you can give. `all` — the whole tenant, for the people the web ticket list admits.\n",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "assigned",
                "team",
                "available",
                "approvals",
                "all"
              ]
            }
          },
          {
            "name": "assigned_to",
            "in": "query",
            "required": false,
            "description": "`me` — only tickets assigned to you",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ]
            }
          },
          {
            "name": "requested_by",
            "in": "query",
            "required": false,
            "description": "`me` — only tickets you requested",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ]
            }
          },
          {
            "name": "unassigned",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "escalation_active",
            "in": "query",
            "required": false,
            "description": "`true` — escalated and not yet resolved, closed, cancelled or rejected: the set the dashboard \"Escalated\" tile counts",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "sla_breached",
            "in": "query",
            "required": false,
            "description": "`true` — SLA deadline passed on a ticket not yet resolved, closed, cancelled or rejected; `false` — the rest",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "approver",
            "in": "query",
            "required": false,
            "description": "`me` — only tickets waiting for an approval you can give",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ]
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "description": "Limit to one service desk (department)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by ticket status. Repeat it (`status[]=assigned&status[]=in_progress`) for several; `escalated` is the escalation flag, OR-ed with any statuses given.",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "enum": [
                    "submitted",
                    "queued",
                    "assigned",
                    "in_progress",
                    "pending_approval",
                    "escalated",
                    "resolved",
                    "closed",
                    "cancelled",
                    "rejected"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "enum": [
                      "submitted",
                      "queued",
                      "assigned",
                      "in_progress",
                      "pending_approval",
                      "escalated",
                      "resolved",
                      "closed",
                      "cancelled",
                      "rejected"
                    ]
                  }
                }
              ]
            }
          },
          {
            "name": "escalated",
            "in": "query",
            "required": false,
            "description": "`true` — escalated tickets (any status); `false` — tickets that are not escalated",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "description": "Filter by priority level. Repeat it (`priority[]=high&priority[]=critical`) for several. Ignored for a caller the tenant hides priority from.",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "enum": [
                    "low",
                    "medium",
                    "high",
                    "critical"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ]
                  }
                }
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Staff sets only (`assigned`, `team`, `available`, `all`) — the web lists' own search.\nOn `assigned` / `team` / `available`: the title. On `all`: title, description, or a ticket number (`1169` / `#1169`).\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Staff sets only — the request type slug (e.g. `it_support`). Repeat it for several. On `all`, the web list's `type` is accepted too and wins when both are sent.",
            "schema": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            }
          },
          {
            "name": "requester",
            "in": "query",
            "required": false,
            "description": "Staff sets only — the requester's user id. Never matches an anonymous ticket, whose requester is withheld (`requester.id` is null).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "asset_pro_asset_id",
            "in": "query",
            "required": false,
            "description": "On `all` only — the tickets linked to one Asset Pro asset (the asset page's list), never an anonymous one. An id naming no asset of this organization matches nothing.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "open_days",
            "in": "query",
            "required": false,
            "description": "Staff sets only — still open and submitted more than this many days ago",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "due_soon",
            "in": "query",
            "required": false,
            "description": "Staff sets only — on the ticket's due date (`due_at`): `24h` due within 24 hours, `7d` within 7 days,\n`custom` due on or before `due_before`. Any other value is ignored.\n",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "custom"
              ]
            }
          },
          {
            "name": "due_before",
            "in": "query",
            "required": false,
            "description": "With `due_soon=custom` — a date (YYYY-MM-DD); an unreadable date means today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "`scope=all` only — the web ticket list's saved views. `my_department` — tickets in the\ndepartments you manage; `awaiting_my_approval` — waiting for your approval (the set the\ndashboard widget counts; open to anyone with requests to approve, even without the rest\nof the full list); `past_sla` — the SLA deadline has passed; `unassigned` — no one assigned.\nAny other value is ignored.\n",
            "schema": {
              "type": "string",
              "enum": [
                "my_department",
                "awaiting_my_approval",
                "past_sla",
                "unassigned"
              ]
            }
          },
          {
            "name": "assigned",
            "in": "query",
            "required": false,
            "description": "`scope=all` only — the web list's \"Assigned to\" filter: a person's user id (numeric) keeps the tickets assigned to\nthem; `unassigned` — no one assigned;\n`waiting` — tickets with a requester's reply waiting on you.\n",
            "schema": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string",
                  "enum": [
                    "unassigned",
                    "waiting"
                  ]
                }
              ]
            },
            "example": 77
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "description": "`scope=all` only — the same as a numeric `assigned` (the URL-only name, kept for links already written); it wins when both are sent.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "`scope=all` only — tickets routed to one support team.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Default 100, max 100",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of tickets retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "scope": {
                      "type": "string",
                      "description": "The set listed (an unknown `scope` lists `mine`)"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Tallies over the whole filtered set, before paging",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "open": {
                          "type": "integer"
                        },
                        "resolved": {
                          "type": "integer"
                        },
                        "overdue": {
                          "type": "integer"
                        },
                        "aging": {
                          "type": "integer"
                        }
                      }
                    },
                    "count": {
                      "type": "integer",
                      "example": 5,
                      "description": "Total matching tickets (all pages)"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupportTicketSummary"
                      }
                    },
                    "assignee_options": {
                      "type": "array",
                      "description": "`scope=all` only — the people the web list's \"Assigned to\" filter offers: whoever the tickets of\nthe current `view` and `department_id` are assigned to, by name (the other filters do not narrow it).\nSend an `id` as `assigned` to list that person's tickets. A person picked with `assigned` who still\nholds a ticket in the list but none in the view is included. Under \"Hide Assignee from Requestor\",\na caller who is not shown other people's assignees is offered only themself.\n",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 77
                          },
                          "name": {
                            "type": "string",
                            "example": "Priya Shah"
                          },
                          "active": {
                            "type": "boolean",
                            "description": "`false` for someone no longer an active member of this organization (shown \"(left)\" on the web)"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`scope=all` for a caller without the full ticket list, or `scope=team`\nfor someone who is not Service Desk staff — or the token lacks\n`read:service_desk`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "forbidden"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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\nDraw the form from `GET /service_desk/service_catalog`: each type's\n`form` block says which fields to show and require, and the top-level\n`form` block carries the form-wide rules (priority, anonymous, KB).\n\n**Intake rules are enforced for the fields you send.** Urgency reason,\nestimated cost / business justification and custom fields each have\nrequired rules (see the catalog). A rule applies once the request\ncarries its field's key — so send the key (blank if unanswered) for every\nfield your form draws, and a client that does not draw a field is not\nrefused over it.\n\n**Idempotency.** Send an `Idempotency-Key` header per Submit. A retry\nwith the same key and body replays the first 201 (same ticket id,\n`X-Idempotency-Cached: true`) instead of filing again; the same key with a\ndifferent body answers 409 `idempotency_conflict`; a retry while the first\nis still running answers 409 `idempotency_in_progress` with `Retry-After`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "One key per Submit; retries reuse it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ticket"
                ],
                "properties": {
                  "ticket": {
                    "type": "object",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "description": "Brief title of the issue",
                        "example": "Laptop overheating issue"
                      },
                      "description": {
                        "type": "string",
                        "description": "Detailed description of the problem. Required only when the type's `form.description_required` is true.",
                        "example": "My laptop has been overheating when running multiple applications"
                      },
                      "request_type": {
                        "type": "string",
                        "description": "A `type` slug from `GET /service_desk/service_catalog`. Not a\nfixed list — tenants define their own request types.\n",
                        "example": "it_support"
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "low",
                          "medium",
                          "high",
                          "critical"
                        ],
                        "description": "Send it only when the catalog's `form.priority.shown` is true.\nWhen the tenant sets priority itself (priority levels off, or a\nsystem / agent priority mode) the server replaces it.\n",
                        "example": "medium"
                      },
                      "location_id": {
                        "type": "integer",
                        "nullable": true,
                        "example": 1
                      },
                      "service_department_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Ignored unless it is a department of this tenant."
                      },
                      "estimated_cost": {
                        "type": "number",
                        "nullable": true,
                        "description": "A layout field (`form.fields.estimated_cost`). Also drives the cost half of the approval rule."
                      },
                      "business_justification": {
                        "type": "string",
                        "nullable": true,
                        "description": "A layout field (`form.fields.business_justification`)."
                      },
                      "due_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true,
                        "description": "A layout field (`form.fields.due_at`) — \"Needed by\"."
                      },
                      "urgency_reason": {
                        "type": "string",
                        "nullable": true,
                        "description": "\"Why is this urgent?\" — required for the priorities in the\ncatalog's `form.priority.urgency_reason_required_for` when\nthis key is sent together with a `priority`.\n"
                      },
                      "anonymous": {
                        "type": "boolean",
                        "description": "File without a requester name. Ignored unless the catalog's\n`form.anonymous_allowed` is true. The filer still sees the\nticket in their own list.\n"
                      },
                      "custom_fields": {
                        "type": "object",
                        "additionalProperties": true,
                        "description": "Answers keyed by each custom field's `field_name` (from the\ntype's `form.custom_fields`). A multiselect takes an array of\nstrings; a people / location / department field takes the\ndisplay name. Unknown keys, and internal-only fields for a\nrequester, are dropped.\n",
                        "example": {
                          "device_type": "Laptop",
                          "business_impact": "I cannot work at all"
                        }
                      },
                      "requester_id": {
                        "type": "integer",
                        "description": "File FOR this person. Only when `GET /config` says `permissions.can_create_on_behalf` — otherwise the request is refused with 403 and nothing is filed. A person who is not in this organization is a 422. Sending the caller's own id is the same as leaving it out."
                      },
                      "vendor_id": {
                        "type": "integer",
                        "description": "Kept only for the request types in `GET /config` → `vendor_request_types`."
                      }
                    }
                  },
                  "notify_user_ids": {
                    "type": "array",
                    "description": "People to loop in (\"Notify others\") — ignored on an anonymous ticket or when the type's form hides the field; at most 10.",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same `ticket[...]` fields and `notify_user_ids[]`, plus `attachments[]` files (10 MB each, up to the tenant's limit)."
              }
            }
          }
        },
        "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"
                    },
                    "attachments_warning": {
                      "type": "string",
                      "nullable": true,
                      "description": "Null when every file in `attachments[]` was stored. Otherwise a\nsentence naming what was not — e.g. \"2 of 3 files were attached.\nbig.png exceeds the 10 MB size limit.\" The files that passed ARE\nattached; the ticket is filed either way.\n"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Two shapes. The caller may not file this request type, or sent a\n`requester_id` without `permissions.can_create_on_behalf`:\n`{success: false, error: {code: \"forbidden\", message: \"<sentence>\"}}` (shown below). Service Desk not\nenabled, or the token lacks `write:service_desk`: the standard\n`{error: {code, message}}` envelope (codes `service_desk_not_enabled`,\n`insufficient_permissions`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "forbidden"
                        },
                        "message": {
                          "type": "string",
                          "example": "You don't have access to submit that type of request."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict (key reused with a different body, or the first request is still running)",
            "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. Each entry of `errors` is a readable sentence. A `request_type` of `grievance` is also refused here (`error` only) — grievances are filed through POST /service_desk/grievances — and so is a `requester_id` that is not a person in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "validation_failed"
                        },
                        "message": {
                          "type": "string",
                          "example": "Failed to create ticket"
                        }
                      }
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Device type is required",
                        "Urgency reason can't be blank"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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. Repeat it (`status[]=…`) for several; `escalated` is the escalation flag, OR-ed with any statuses given.",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "enum": [
                    "submitted",
                    "queued",
                    "assigned",
                    "in_progress",
                    "pending_approval",
                    "escalated",
                    "resolved",
                    "closed",
                    "cancelled",
                    "rejected"
                  ]
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "enum": [
                      "submitted",
                      "queued",
                      "assigned",
                      "in_progress",
                      "pending_approval",
                      "escalated",
                      "resolved",
                      "closed",
                      "cancelled",
                      "rejected"
                    ]
                  }
                }
              ]
            }
          },
          {
            "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"
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "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\nAlso carries the **New Request form rules**, read from the same tenant\nsettings the web form uses: `form` (form-wide) and, on each\n`service_types` entry, `form` (that type's fields, templates and custom\nfields), already resolved for this viewer.\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",
                      "description": "The most-filed types, for a \"popular\" strip. Same shape as `service_types` entries WITHOUT `form` — read the form rules from the matching `service_types` entry.",
                      "items": {
                        "$ref": "#/components/schemas/ServiceCatalogType"
                      }
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "form": {
                      "$ref": "#/components/schemas/ServiceDeskIntakeForm"
                    }
                  }
                }
              }
            }
          },
          "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\nWith no `q`, browse instead: the articles the caller may read,\nnewest first, 20 a page (`page`, `has_more`), optionally narrowed by\n`category`, or by `request_type` (the category that type's help usually\nlives in — falling back to all articles when that category is empty).\n\nEvery page says whether the caller sees unpublished work\n(`can_see_unpublished`) — the web Knowledge Base's rule: a tenant or\nService Desk admin sees every status, a KB approver the review queue,\nan author their own drafts and rejected work, everyone else published\narticles only. When true, `statuses` lists the status chips the web\nKnowledge Base's Status filter offers, the list (browse and search)\nincludes the unpublished articles the caller may see, and `status`\nnarrows it to one status. Each article result carries its `status`\nand `status_label`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search query. Leave out to browse.",
            "schema": {
              "type": "string",
              "example": "VPN password reset"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Browse only",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Browse only",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "request_type",
            "in": "query",
            "required": false,
            "description": "Browse only",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One article status (browse and search). A Knowledge Base entry (FAQ, document, video) is live content and is kept under `published` only. Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "pending_review",
                "approved",
                "published",
                "rejected",
                "archived"
              ]
            }
          },
          {
            "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",
                      "nullable": true,
                      "description": "Null when browsing (no `q`)"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Results in this response"
                    },
                    "page": {
                      "type": "integer",
                      "description": "Browse only"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Browse only — another page exists"
                    },
                    "can_see_unpublished": {
                      "type": "boolean",
                      "description": "The caller sees unpublished work (the staff variant — status chips, a status pill per card)"
                    },
                    "statuses": {
                      "type": "array",
                      "description": "Only when `can_see_unpublished` — the status chips to offer, in order",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "pending_review"
                          },
                          "label": {
                            "type": "string",
                            "example": "Pending review"
                          }
                        }
                      }
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/KbSearchResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A non-scalar `q` (e.g. `q[]=x`), or a `status` that is not an article status"
          },
          "404": {
            "description": "The Knowledge Base is turned off for this Service Desk (`GET /service_desk/config` → `features.knowledge_base` is false). The article and rating endpoints answer the same."
          }
        }
      }
    },
    "/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}/claim": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Claim an unowned ticket",
        "description": "Take a submitted or queued ticket nobody holds yet — the web ticket\npage's Claim Ticket. Offered when the ticket's `permissions.can_claim`\nis true.\n\nRules:\n- Service Desk staff only (an agent, an admin, or the Service Desk app admin).\n- A queued ticket is claimed only from a team queue the caller is on;\n  business admins and super admins may claim from any queue.\n- When prevent-self-service is enabled, the requester cannot claim\n  their own ticket.\n- Fires the same assignment notice, history and timeline line as the web app.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Claimed — `success` true, `message`, and the updated `ticket`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff, the caller's own ticket under prevent-self-service, or a team queue the caller is not on"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Already claimed, pending approval, or no longer unowned; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/comments": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get ticket comments",
        "description": "Retrieve the comments and updates on a support ticket, oldest first, a page at a time. Use this to view ticket comments, see conversation history, check what was discussed, read agent responses, view ticket updates, or see comment thread.\n\nPaged at 100 comments by default (and at most). A thread of up to 100 comments comes back whole; a longer one returns `has_more: true` — request the next `page` until it is false. `comments_count` is always the total number of visible comments.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Default 100, max 100",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the ticket's comments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "ticket_id": {
                      "type": "integer"
                    },
                    "comments_count": {
                      "type": "integer",
                      "description": "Total visible comments on the ticket (all pages)"
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TicketComment"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "Another page of comments exists"
                    }
                  }
                }
              }
            }
          },
          "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\nSend an `Idempotency-Key` per send: a retry with the same key replays the\nfirst 201 instead of posting (and notifying) twice.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ticket ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "One key per send; retries reuse it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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). Only Service Desk staff,\napp admins and the ticket's assignee may post one; anyone else who\nsends `internal: true` is refused with 403 rather than having the\nnote published to the requester.\n",
                        "default": false
                      }
                    }
                  },
                  "saved_reply_id": {
                    "type": "integer",
                    "description": "Staff only — the saved reply this comment was written from, to count its use."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same fields, plus files. Mentions go in `comment[content]` as `@[Name](mention:<user id>)`, as on the web.",
                "properties": {
                  "comment[content]": {
                    "type": "string"
                  },
                  "comment[internal]": {
                    "type": "boolean"
                  },
                  "saved_reply_id": {
                    "type": "integer"
                  },
                  "attachments[]": {
                    "type": "array",
                    "description": "Up to the tenant's per-request attachment limit; 10 MB each.",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment added successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/TicketComment"
                    },
                    "attachments_warning": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when some files were not attached (over the limit, refused type, or attachments switched off). The comment itself was posted."
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/SupportTicketDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The caller asked for an internal note but may not post one on this ticket",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "forbidden"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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\nAlso approve / reject (with `approval_notes` / `rejection_reason`) and\ncancel / reinstate (with `cancellation_reason` / `reinstate_reason`).\n`permissions.allowed_status_actions` on the ticket lists the actions\nthis caller may take right now.\n\nClosing, cancelling or rejecting a ticket (`action_type` `close`, `cancel` or\n`reject`) is delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\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",
                      "start_progress",
                      "revert_to_assigned",
                      "approve",
                      "reject",
                      "cancel",
                      "reinstate"
                    ],
                    "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. `reopen` on a grievance whose workflow is\nactive is refused with \"A grievance isn't reopened — an administrator\nmoves it back to an earlier stage, with a reason.\"\n"
          }
        }
      }
    },
    "/service_desk/{id}/comments/{comment_id}": {
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Edit a comment",
        "description": "The author within 15 minutes of posting, or an admin. Not on a rejected or cancelled ticket. `internal` may change only for someone who may write internal notes.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "comment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "object",
                    "properties": {
                      "content": {
                        "type": "string"
                      },
                      "internal": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/TicketComment"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the author inside the window, and not an admin"
          },
          "404": {
            "description": "Ticket or comment not found for this caller"
          },
          "422": {
            "description": "The ticket is read-only (rejected or cancelled), or validation failed"
          }
        }
      },
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Delete a comment",
        "description": "The author within 5 minutes of posting, or an admin. Not on a rejected or cancelled ticket.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "comment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "deleted_comment_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the author inside the window, and not an admin"
          },
          "404": {
            "description": "Ticket or comment not found for this caller"
          },
          "422": {
            "description": "The ticket is read-only (rejected or cancelled)"
          }
        }
      }
    },
    "/service_desk/{id}/activity": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Ticket activity (comments and history)",
        "description": "The ticket's timeline as the web page draws it: top-level comments and\nlifecycle milestones (submitted, assigned, priority changed, resolved, …)\nmerged in time order and filtered for the caller — internal notes only\nfor staff and the assignee, staff-only history only for staff, and an\nagent's name left out of a milestone when the tenant hides the assignee.\nReturns the newest `limit` entries, oldest first.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many of the newest entries to return (default 50, max 200).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Activity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "ticket_id": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "Older entries exist beyond `limit`."
                    },
                    "activity": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TicketActivityEntry"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket not found or access denied"
          }
        }
      }
    },
    "/service_desk/saved_replies": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Saved replies (staff)",
        "description": "The composer's canned answers, most used first — Service Desk staff only. Placeholders `{{requester_name}}`, `{{ticket_id}}`, `{{agent_name}}` are left for the app to fill.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved replies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "saved_replies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          },
                          "body": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string",
                            "nullable": true
                          },
                          "usage_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff"
          }
        }
      }
    },
    "/service_desk/{id}/followers": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Add a person to the ticket",
        "description": "The support side or the requester, while the ticket is live; the person is notified. Returns `followers`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "Who to add"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/followers/{user_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Remove a person from the ticket",
        "description": "Staff remove anyone; anyone may remove themselves.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/rating": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Rate the service (requester)",
        "description": "Once, after the ticket is resolved or closed. 1-5; `feedback` is required for 3 or below.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rating": {
                    "type": "integer",
                    "description": "1 to 5"
                  },
                  "feedback": {
                    "type": "string",
                    "description": "Required for 3 or below"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/confirm_resolution": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Confirm a suggested answer solved it",
        "description": "The requester (or an admin) confirms the comment the Service Desk autopilot posted, which resolves the ticket. Not the same as close.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment_id": {
                    "type": "integer",
                    "description": "The pending confirmation comment"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/escalate": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Escalate",
        "description": "Staff or the assignee, on a ticket that can be escalated. Without escalate_to_id the tenant escalation target is used.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why"
                  },
                  "escalate_to_id": {
                    "type": "integer",
                    "description": "Optional person"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/de_escalate": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Remove an escalation",
        "description": "An admin, the assignee, or the person it was escalated to.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Optional"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/merge_targets": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Tickets this one could merge into (staff)",
        "description": "Open tickets matching q (a number, #1234, or title words). Grievances only merge with grievances.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/merge": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Merge into another ticket (staff)",
        "description": "The merge service re-checks every rule and returns the refusal to show. This ticket is cancelled as merged.\n\nCancelling this ticket makes the merge delete-level (owner decision\n2026-10-04, #6721): on top of `write:service_desk` it requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target_id": {
                    "type": "integer",
                    "description": "The ticket to merge into"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/clone": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Prefilled fields for a copy (staff)",
        "description": "Nothing is saved: POST the returned `ticket` fields to /service_desk to file the copy.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/time_entries": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Log time (staff)",
        "description": "1-1440 minutes; worked_on defaults to today when omitted, cannot be in the future, and must be YYYY-MM-DD (any other value is a 422, never today). Send an `Idempotency-Key` so a retry does not log the time twice.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minutes": {
                    "type": "integer",
                    "description": "Minutes worked"
                  },
                  "worked_on": {
                    "type": "string",
                    "description": "Date, YYYY-MM-DD"
                  },
                  "note": {
                    "type": "string",
                    "description": "Up to 255 characters"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/time_entries/{entry_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Remove a time entry",
        "description": "Its author, or an admin.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "entry_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/parts": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Request parts from inventory (staff)",
        "description": "Needs the Procurement app on and open to the caller. Returns the updated `parts`. Send an `Idempotency-Key` so a retry does not request the part twice.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "inventory_item_id": {
                    "type": "integer",
                    "description": "Item"
                  },
                  "quantity": {
                    "type": "integer",
                    "description": "At least 1"
                  },
                  "cost_center": {
                    "type": "string",
                    "description": "Optional"
                  },
                  "delivery_location": {
                    "type": "string",
                    "description": "Optional"
                  },
                  "station_id": {
                    "type": "integer",
                    "description": "Optional"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional"
                  },
                  "collect_at_location_id": {
                    "type": "integer",
                    "description": "Optional. Where the parts wait to be collected — one of `parts.collect_points` from the ticket. Any other id is refused."
                  },
                  "requested_for_id": {
                    "type": "integer",
                    "description": "Optional. Who the parts are for, and so who is alerted when they are ready — one of `parts.recipients` from the ticket (the caller, or Service Desk staff on the job). Defaults to the caller; anyone else is refused."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true with the updated `parts`: `items` (issued and returned movements) and `requests` (open and declined requests, each with `requested_for`, `collect_at`, `picking_by`, `can_cancel` and per-line `quantity`, `issued`, `declined`, `remaining`, `estimated_cost`), and the choices this endpoint accepts: `collect_points` ([{id, name}]; empty when the tenant has none), `last_collect_point_id` and `recipients` ([{id, name, me}])"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/parts/search": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Find a part to request (staff)",
        "description": "Active inventory items whose name or SKU contains `q`, or that a scanned barcode, case code, QR label or exact SKU in `q` names (that match comes first, with `matched_code`). Out-of-stock parts are included. Each part carries its unit cost, total on hand, and `places` — on hand per top-level stockroom, most first. Needs the Procurement app on and open to the caller. Fewer than 2 characters returns an empty list.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Name, SKU or scanned code"
          }
        ],
        "responses": {
          "200": {
            "description": "`success` true and `parts` — up to 20 of `{ id, name, sku, unit_cost, on_hand, places: [{ id, name, on_hand }], matched_code }`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Procurement is not enabled; `error` says so"
          }
        }
      }
    },
    "/service_desk/{id}/parts/sources": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "One part's cost and where it is held (staff)",
        "description": "The unit cost and on hand per top-level stockroom of an active inventory item, before requesting it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "inventory_item_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`success` true and `part` — `{ id, name, sku, unit_cost, on_hand, places: [{ id, name, on_hand }] }`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket, or an active part with that id, not found"
          },
          "422": {
            "description": "Procurement is not enabled; `error` says so"
          }
        }
      }
    },
    "/service_desk/{id}/parts/requests/{requisition_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Withdraw a parts request",
        "description": "Only before the warehouse starts picking.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "requisition_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/parts/requests/{requisition_id}/dismiss": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Dismiss a declined parts request",
        "description": "Clears it off the ticket.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "requisition_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/parts/{transaction_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Return an issued part",
        "description": "Writes a returned movement.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "transaction_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done — `success` true, usually with the updated `ticket`"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Refused in this state; `error` says why"
          }
        }
      }
    },
    "/service_desk/{id}/project": {
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Set the project this ticket bills to (staff)",
        "description": "The web Parts card's \"Bills to\". Every part requested afterwards inherits the project; parts already issued keep the one they were charged to. Send a blank `procurement_budget_id` to clear it. Needs the Procurement app on and open to the caller, like the other parts endpoints. Returns the updated `parts`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "procurement_budget_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "One of `parts.billable_projects` from the ticket; blank or null clears the project. Any other id is refused."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done — `success` true, `message`, and the updated `parts` (with `project`, `billable_projects` and `suggested_project`)"
          },
          "403": {
            "description": "Not permitted for this caller"
          },
          "404": {
            "description": "Ticket not found or not visible"
          },
          "422": {
            "description": "Procurement is not enabled, or the project is not one this ticket may bill to; `error` says why"
          }
        }
      }
    },
    "/service_desk/support_teams": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Active support teams",
        "description": "For the assign / transfer picker. Assigning to a team only accepts an active one.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Teams",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "support_teams": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "tier_level": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/agents": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Agents a ticket can be assigned to",
        "description": "The person half of the assign / transfer picker (the team half is GET /service_desk/support_teams) —\nthe same people the web ticket page offers: active members of an active support team, or people granted\nagent rights in Settings → Agents. Admins and managers are not listed; they take or claim tickets themselves.\nSend one as `assignee_id` to POST /service_desk/{id}/assign. Service Desk staff only.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Agents, by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "agents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The caller is not Service Desk staff, or the token lacks `read:service_desk`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "forbidden"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/dashboard": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Agent dashboard (staff)",
        "description": "The agent home screen: workload, tiles and short lists. Each tile rule is spelled out in `tile_rules`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "department": {
                      "type": "object",
                      "nullable": true,
                      "description": "The desk `department_id` narrowed to, else null",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "workload": {
                      "type": "object",
                      "properties": {
                        "total_assigned": {
                          "type": "integer"
                        },
                        "open": {
                          "type": "integer"
                        },
                        "overdue": {
                          "type": "integer",
                          "description": "Open, and the earlier of due date and SLA deadline has passed"
                        },
                        "due_today": {
                          "type": "integer",
                          "description": "Open, due (earlier of due date and SLA deadline) from now until midnight"
                        },
                        "due_tomorrow": {
                          "type": "integer"
                        },
                        "capacity": {
                          "type": "integer"
                        },
                        "utilization": {
                          "type": "integer",
                          "description": "Percent of capacity in use, capped at 100"
                        }
                      }
                    },
                    "tiles": {
                      "type": "object",
                      "properties": {
                        "my_open": {
                          "type": "integer"
                        },
                        "breached": {
                          "type": "integer"
                        },
                        "escalated": {
                          "type": "integer"
                        },
                        "team_queue": {
                          "type": "integer",
                          "description": "Routed to your teams, nobody has picked it up yet"
                        },
                        "available": {
                          "type": "integer"
                        },
                        "unclaimed": {
                          "type": "integer",
                          "description": "team_queue + available"
                        },
                        "pending_my_approval": {
                          "type": "integer"
                        },
                        "ai_drafts": {
                          "type": "integer"
                        }
                      }
                    },
                    "tile_rules": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "sections": {
                      "type": "object",
                      "properties": {
                        "my_tickets": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SupportTicketSummary"
                          }
                        },
                        "team_queue": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SupportTicketSummary"
                          }
                        },
                        "available": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SupportTicketSummary"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff"
          }
        }
      }
    },
    "/service_desk/ai_drafts": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "AI-drafted replies waiting to be sent (staff)",
        "description": "Drafts on a grievance appear only for people who may open that grievance.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Drafts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "pending": {
                          "type": "integer"
                        },
                        "sent_today": {
                          "type": "integer"
                        },
                        "rejected_today": {
                          "type": "integer"
                        }
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "drafts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string"
                          },
                          "content": {
                            "type": "string"
                          },
                          "final_content": {
                            "type": "string",
                            "description": "What Send delivers: the agent's edit when there is one (kept when a failed send returned the draft to pending), else content."
                          },
                          "last_send_error": {
                            "type": "string",
                            "nullable": true,
                            "description": "Why the last Approve & Send failed, when a failed send returned this draft to pending."
                          },
                          "confidence": {
                            "type": "number",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "ticket": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "title": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff"
          }
        }
      }
    },
    "/service_desk/ai_drafts/{draft_id}/send": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Approve and send an AI draft (staff)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "edited_content": {
                    "type": "string",
                    "description": "Send this text instead of the draft as written"
                  },
                  "channel": {
                    "type": "string",
                    "description": "Defaults to email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent"
          },
          "404": {
            "description": "Not found, already processed, or on a ticket you may not open"
          },
          "422": {
            "description": "Could not be sent; `error` says why"
          }
        }
      }
    },
    "/service_desk/ai_drafts/{draft_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Discard an AI draft (staff)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "reason",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Discarded"
          },
          "404": {
            "description": "Not found, already processed, or on a ticket you may not open"
          }
        }
      }
    },
    "/service_desk/kb/articles/{article_id}": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Read a knowledge base article",
        "description": "The article as the web page shows it — the reader's KB-role rules, published only, the body through the page's safe-HTML allow-list. Counts a view. Send `from=ai_chat` when opened from the AI assistant.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "article_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "ai_chat"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "article": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "category": {
                          "type": "string",
                          "nullable": true
                        },
                        "content_html": {
                          "type": "string",
                          "description": "Sanitised HTML"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "status": {
                          "type": "string",
                          "description": "Workflow status, an open set of strings; always published here"
                        },
                        "status_label": {
                          "type": "string",
                          "description": "The status as a person reads it (\"Published\")"
                        },
                        "view_count": {
                          "type": "integer"
                        },
                        "ratings": {
                          "type": "object",
                          "description": "\"Was this helpful?\" votes; the web's Helpfulness is helpful / (helpful + not_helpful).",
                          "properties": {
                            "helpful": {
                              "type": "integer"
                            },
                            "not_helpful": {
                              "type": "integer"
                            }
                          }
                        },
                        "my_rating": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "helpful",
                            "not_helpful"
                          ]
                        },
                        "can_rate": {
                          "type": "boolean",
                          "description": "May vote \"Was this helpful?\" (POST /api/v1/service_desk/kb/articles/{article_id}/rating) — false for a token without write:service_desk"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, not published, or not for this reader"
          }
        }
      }
    },
    "/service_desk/kb/articles/{article_id}/rating": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Was this article helpful?",
        "description": "One vote per person, which they may switch.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "article_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "helpful"
                ],
                "properties": {
                  "helpful": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "new_vote",
                        "switched",
                        "duplicate"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "my_rating": {
                      "type": "string",
                      "enum": [
                        "helpful",
                        "not_helpful"
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found for this reader"
          },
          "422": {
            "description": "`helpful` missing or not true/false — no vote is recorded"
          }
        }
      }
    },
    "/service_desk/config": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Service Desk settings for the app",
        "description": "Everything the app needs to draw Service Desk screens: priorities,\nstatuses (with `is_open` / `is_terminal`), departments, active support\nteams, locations, vendors and the request types that ask for one, saved\nreplies (staff), attachment limits, rating rules, feature and module\nswitches, the caller's permissions, and the form-wide New Request rules.\nThe per-request-type form rules stay on GET /service_catalog.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "priorities": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "color": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "statuses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "color": {
                            "type": "string"
                          },
                          "is_open": {
                            "type": "boolean"
                          },
                          "is_terminal": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "departments": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "support_teams": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "vendors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "vendor_request_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "saved_replies": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "attachments": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "max_per_request": {
                          "type": "integer"
                        },
                        "max_file_bytes": {
                          "type": "integer"
                        }
                      }
                    },
                    "rating": {
                      "type": "object",
                      "properties": {
                        "min": {
                          "type": "integer"
                        },
                        "max": {
                          "type": "integer"
                        },
                        "feedback_required_at_or_below": {
                          "type": "integer"
                        },
                        "survey_enabled": {
                          "type": "boolean"
                        }
                      }
                    },
                    "features": {
                      "type": "object"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "staff": {
                          "type": "boolean"
                        },
                        "can_create_on_behalf": {
                          "type": "boolean"
                        },
                        "can_view_all_tickets": {
                          "type": "boolean"
                        }
                      }
                    },
                    "new_request": {
                      "type": "object",
                      "description": "ServiceDesk::IntakeForm#form — priority, anonymous, approval threshold, KB suggestions"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/my_tickets": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get my tickets widget (role-based tabs)",
        "description": "Dashboard widget endpoint. Returns service desk tickets organized into\nrole-based tabs. Each tab includes a total_count (for badge display) and\na small preview of recent tickets.\n\nOnly tabs applicable to the user's role are returned:\n- created_by_me: always included (all users) — sorted by latest created first\n- assigned_to_me: help desk agents, managers, admins, super admins — sorted by escalated first, then priority, then oldest first\n- pending_approval_for_me: managers, admins, super admins — sorted by priority (oldest first)\n\nThis is a preview endpoint — no pagination. Use the main ticket list\nfor paginated browsing (triggered via \"View All\").\n\nEach tab's `label` is display text in the language the client asks for:\nthe `locale` query parameter, else `Accept-Language` (first supported\ntag by q-value), else the caller's own profile language, else the\ntenant's default, else English — whether or not the tenant translates\nits web pages. The response carries `Vary: Accept-Language`. Branch on\n`key`, never on `label`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language the client's own interface is in (e.g. `fr`). Takes precedence over `Accept-Language` for `label`. An unsupported code is ignored.",
            "schema": {
              "type": "string"
            },
            "example": "fr"
          },
          {
            "name": "per_tab",
            "in": "query",
            "description": "Number of preview tickets per tab (default 5, max 10)",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Widget data retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyTicketsWidgetResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/service_desk/problems": {
      "get": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "List problem records",
        "description": "The Problems list with the web's filters and search, newest first, plus the\nstats cards (total, open, known errors, average linked incidents).\n`view=known_errors` lists published Known Errors instead, searched on title,\nworkaround and known-error description.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One status, or several as status[]",
            "schema": {
              "type": "string",
              "enum": [
                "identified",
                "investigating",
                "root_cause_found",
                "fix_in_progress",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "description": "One priority, or several as priority[]",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "network",
                "hardware",
                "software",
                "security",
                "process",
                "other"
              ]
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "known_error",
            "in": "query",
            "required": false,
            "description": "Only problems published as Known Errors",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Matches title or description",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "known_errors"
              ]
            }
          },
          {
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of problems",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "open": {
                          "type": "integer"
                        },
                        "known_errors": {
                          "type": "integer"
                        },
                        "avg_linked": {
                          "type": "number"
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "problems": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskProblemSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Create a problem record",
        "description": "Starts in `identified`, numbered PRB-YYYY-NNNN, with the caller as\n\"identified by\". Notifies the assignee when one is set. Fields may be sent\nat the top level or nested under `problem`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceDeskProblemInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/form_options": {
      "get": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Options for the problem forms",
        "description": "Statuses, priorities, categories, root-cause methods, investigation entry\ntypes, incident link types, the assignee picker (first 100 active people by\nname, plus the problem's own assignee when `problem_id` is given) and\nservice departments.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "problem_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Form options",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "statuses": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "priorities": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "root_cause_methods": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "entry_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "link_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "assignees": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskProblemUser"
                      }
                    },
                    "service_departments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/search_incidents": {
      "get": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Search tickets to link as incidents",
        "description": "Up to 20 tickets, newest first, matched on id or title, excluding those\nalready linked to `problem_id`. Grievances are left out unless the caller\nmay handle grievances.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "problem_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching tickets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "incidents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskProblemIncident"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Show a problem record",
        "description": "The record with its linked incidents (newest 50, grievance-clamped — a\ngrievance is never listed or counted for someone who could not open it),\nconfiguration items, investigation entries and activity timeline (newest\n200 each, oldest first), the fix change request (while Change Management\nis on) and `permissions` — which controls work for this caller now.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The problem",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      },
      "patch": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Edit a problem record",
        "description": "Notifies a newly set assignee. Refused (422) once the problem is closed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceDeskProblemInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/transition": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Move a problem to its next lifecycle step",
        "description": "The lifecycle is strictly identified → investigating → root_cause_found →\nfix_in_progress → resolved → closed; any other move is refused with 422\n(\"Invalid transition from x to y\"). `record_root_cause` needs `root_cause`,\n`resolve` needs `resolution`, `close` needs `closure_notes`. Recording the\nroot cause, resolving and closing notify the people involved.\n`permissions.available_events` on the show response names the legal step.\n\nClosing a problem (the `close` event, or `to_status` `closed`) is delete-level\n(owner decision 2026-10-04, #6721): requires the `destructive:service_desk`\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": [
                      "start_investigation",
                      "record_root_cause",
                      "start_fix",
                      "resolve",
                      "close"
                    ]
                  },
                  "to_status": {
                    "type": "string",
                    "description": "The target status",
                    "in place of `event`": null
                  },
                  "root_cause": {
                    "type": "string"
                  },
                  "root_cause_method": {
                    "type": "string",
                    "enum": [
                      "5_whys",
                      "fishbone",
                      "fault_tree",
                      "pareto",
                      "timeline",
                      "other"
                    ]
                  },
                  "resolution": {
                    "type": "string"
                  },
                  "closure_notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/close_as_duplicate": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Close a problem as a duplicate of another",
        "description": "From any status short of closed, the problem is closed in favour of\n`duplicate_of_id` — another problem of this organization that is not closed\n(list them with GET /service_desk/problems and a `status` filter). Its linked\nincidents move to that problem (one it already holds is dropped here), a\npublished Known Error is retired, both timelines record it, and both\nassignees are told. `closure_notes` is optional. `permissions.can_close_as_duplicate`\non the show response says whether it is offered.\n\nA close, so delete-level like the `close` event (#6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "duplicate_of_id"
                ],
                "properties": {
                  "duplicate_of_id": {
                    "type": "integer",
                    "description": "The problem to keep"
                  },
                  "closure_notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Closed as a duplicate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/reopen": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Reopen a closed problem",
        "description": "A closed problem goes back to investigating. Its last root cause and\nresolution are kept as investigation entries and cleared, so a new root\ncause can be recorded; a duplicate becomes its own problem again (the\nincidents it handed over stay where they went). `reason` is required and is\nrecorded on the timeline. Service Desk admins only (a tenant admin or a\nService Desk app admin) — anyone else gets 403. `permissions.can_reopen` on the\nshow response says whether it is offered.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reopened",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/investigation_entries": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Add an investigation entry",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "entry_type": {
                    "type": "string",
                    "enum": [
                      "note",
                      "finding",
                      "workaround",
                      "root_cause",
                      "action_taken"
                    ]
                  },
                  "content": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/known_error": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Publish as a Known Error",
        "description": "Needs the Knowledge Base approver tier (business admin, Service Desk app\nadmin, or a member of the tenant's KB approver groups) — 403 otherwise.\n`workaround` is required; a closed problem cannot be published.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "workaround"
                ],
                "properties": {
                  "workaround": {
                    "type": "string"
                  },
                  "known_error_description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Published",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      },
      "delete": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Unpublish a Known Error",
        "description": "Same approver tier as publishing. Refused once the problem is closed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unpublished",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/incidents": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Link a ticket as an incident of this problem",
        "description": "A grievance the caller may not handle answers 404, like an unknown ticket.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "help_request_id"
                ],
                "properties": {
                  "help_request_id": {
                    "type": "integer"
                  },
                  "link_type": {
                    "type": "string",
                    "enum": [
                      "caused_by",
                      "related_to",
                      "duplicate_of"
                    ],
                    "default": "caused_by"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Linked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/incidents/{help_request_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "help_request_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Unlink an incident",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unlinked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/configuration_items": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Link a configuration item",
        "description": "404 (\"Configuration Management (CMDB) is not turned on for this Service Desk.\") while the tenant has the CMDB module off.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "configuration_item_id"
                ],
                "properties": {
                  "configuration_item_id": {
                    "type": "integer"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Linked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/configuration_items/{configuration_item_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "configuration_item_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Unlink a configuration item",
        "description": "404 (\"Configuration Management (CMDB) is not turned on for this Service Desk.\") while the tenant has the CMDB module off.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unlinked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDeskProblemEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/problems/{id}/change_request": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Problems"
        ],
        "summary": "Raise the fix change request",
        "description": "Creates a draft normal change from the problem. Offered while Change\nManagement is on (404 otherwise), the problem is at root_cause_found and has\nno fix change yet (422 otherwise).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "change_request": {
                      "$ref": "#/components/schemas/ServiceDeskProblemChangeRequest"
                    },
                    "problem": {
                      "$ref": "#/components/schemas/ServiceDeskProblemDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskProblemError"
          }
        }
      }
    },
    "/service_desk/changes": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "List change requests",
        "description": "Service Desk staff (agents, business admins, Service Desk app admins) see every change in the\ntenant; everyone else sees the changes they raised. `stats` are over the same rows, unfiltered.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One status or a list (`status[]=`)",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "submitted",
                "pending_review",
                "approved",
                "scheduled",
                "implementing",
                "implemented",
                "closed",
                "failed",
                "cancelled"
              ]
            }
          },
          {
            "name": "change_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "standard",
                "normal",
                "emergency"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "risk_level",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "requester_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "required": false,
            "description": "Created on or after (YYYY-MM-DD)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "required": false,
            "description": "Created on or before (YYYY-MM-DD)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search title, change number and description",
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page of change requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "change_requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChangeRequestSummary"
                      }
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "draft": {
                          "type": "integer"
                        },
                        "pending_review": {
                          "type": "integer"
                        },
                        "scheduled": {
                          "type": "integer"
                        },
                        "implementing": {
                          "type": "integer"
                        },
                        "closed": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        }
                      }
                    },
                    "can_create": {
                      "type": "boolean"
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Service Desk not enabled for the caller, or the token lacks `read:service_desk`"
          },
          "404": {
            "description": "Change Management is switched off for this tenant"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Raise a change request (draft)",
        "description": "Anyone who can reach Change Management may raise a change. Fields may be nested under\n`change_request` or sent at the top level. `assignee_id` and `cab_id` are ignored unless the\ncaller is Service Desk staff (use `assign` / `route_to_cab`). Attachments are web-only for now.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeRequestInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get a change request",
        "description": "Readable by staff, the requester, the assignee, and the members and chair of its CAB.\n`permissions` lists the actions the caller may take right now — the buttons the web page shows.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The change request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "change_request": {
                      "$ref": "#/components/schemas/ChangeRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, not visible to the caller, or the module is switched off"
          }
        }
      },
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Edit a change request",
        "description": "Draft or submitted changes only, by the requester or an admin (409 once it has moved on);\na draft may also be edited by anyone who may submit it.\n`assignee_id` and `cab_id` are ignored for non-staff. Only the requester or an admin may set\n`change_type` to `emergency` — an emergency change is approved the moment it is submitted — so\nanyone else gets 403 and nothing is saved (`permissions.can_make_emergency`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeRequestInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "409": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/submit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Submit a change for review",
        "description": "Submits a draft. For a change the CAB rejected (it returns to `submitted` with its CAB still set) this resubmits it to that CAB instead — the web's one Submit button does both. Allowed for the requester, the assignee and Service Desk staff. An emergency change submitted by staff or the CAB chair is fast-tracked to `approved`; a member's emergency change goes to review like any other.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/resubmit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Resubmit a rejected change to its CAB",
        "description": "Only for a change the CAB rejected (status `submitted` with a CAB). Clears last round's ballots and opens a new review round (or auto-approves under the board's policy). 422 for anything else.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/approve": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Approve (CAB decision)",
        "description": "Allowed when the caller may carry the decision (ChangeRequest#can_be_approved_by?): an admin, or a voting CAB member once the board's approval rule is met. Never the requester (admins excepted).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comments": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/reject": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Reject and return to the requester",
        "description": "Same gate as approve. The change goes back to `submitted` and keeps its CAB, so the requester can resubmit.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rejection_reason"
                ],
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "description": "Required — blank is a 422"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/request_more_info": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Ask the requester for more information",
        "description": "Same gate as approve. Records the request on the caller's ballot and the timeline.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "details"
                ],
                "properties": {
                  "details": {
                    "type": "string",
                    "description": "Required — blank is a 422"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/vote": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Cast a CAB ballot",
        "description": "Voting members (chair or member, not observer) of the change's CAB, while it is `pending_review` and they have not voted. `outcome` is `approved`/`rejected` when this ballot carried the change under the board's rule.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "decision"
                ],
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "rejected",
                      "deferred",
                      "more_info_requested"
                    ]
                  },
                  "comments": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/route_to_cab": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Route a submitted change to a CAB",
        "description": "Service Desk staff. Only a `submitted` change with no CAB. Auto-approves when the board's auto-approval is on and the risk score is within its threshold; otherwise opens CAB review.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cab_id"
                ],
                "properties": {
                  "cab_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/assign": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Assign the change",
        "description": "Service Desk staff. The assignee must be a Service Desk agent or admin (422 otherwise).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assignee_id"
                ],
                "properties": {
                  "assignee_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/schedule": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Schedule an approved change",
        "description": "Staff or the assignee. Only an `approved` change. Both times ISO 8601, end after start (422 otherwise). A blackout window in `block` mode refuses the schedule (422); in `warn` mode it is booked and the overlap comes back in `warnings`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scheduled_start_at",
                  "scheduled_end_at"
                ],
                "properties": {
                  "scheduled_start_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "scheduled_end_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/start_implementation": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Start implementation",
        "description": "Staff or the assignee. `scheduled` → `implementing`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/complete_implementation": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Complete implementation",
        "description": "Staff or the assignee. `implementing` → `implemented`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "completion_notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/fail_implementation": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Mark implementation failed",
        "description": "Staff or the assignee. `implementing` → `failed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "failure_notes"
                ],
                "properties": {
                  "failure_notes": {
                    "type": "string",
                    "description": "Required — blank is a 422"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/close": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Close the change",
        "description": "Staff or the assignee. From `implemented` or `failed`; `closure_code` and `closure_notes` are required.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "closure_code",
                  "closure_notes"
                ],
                "properties": {
                  "closure_code": {
                    "type": "string",
                    "enum": [
                      "successful",
                      "successful_with_issues",
                      "rolled_back",
                      "failed",
                      "cancelled"
                    ]
                  },
                  "closure_notes": {
                    "type": "string"
                  },
                  "lessons_learned": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/changes/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ChangeRequestId"
        }
      ],
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Cancel the change",
        "description": "Staff, the assignee or the requester. From draft, submitted, pending_review, approved or scheduled.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cancel_reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ChangeRequestActionDone"
          },
          "403": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          },
          "404": {
            "description": "Not found or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/ChangeRequestActionRefused"
          }
        }
      }
    },
    "/service_desk/access_requests": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "List access requests",
        "description": "Access requests the caller may see — the same list the web Access Requests page shows.\nFull admins and Service Desk app admins see every request in the tenant; everyone else\nsees the requests they made, the ones they decided, and the pending ones they are the\ndesignated approver for (location manager, workspace owner, site owner, support team manager).\n\n`needs_my_approval_count` is always returned: the pending requests the caller can approve or\nreject right now (every pending request, for a full admin). `counts` are over the chosen\n`scope`, before the `status` filter.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "`all` (default) — everything you may see. `mine` — requests you made.\n`approvals` — pending requests waiting on your decision.\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "mine",
                "approvals"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "executed",
                "cancelled",
                "expired"
              ]
            }
          },
          {
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page of access requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "all",
                        "mine",
                        "approvals"
                      ]
                    },
                    "access_requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccessRequestSummary"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Requests per status within `scope`",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "example": {
                        "pending": 2,
                        "approved": 0,
                        "rejected": 1,
                        "executed": 4,
                        "cancelled": 0,
                        "expired": 0
                      }
                    },
                    "needs_my_approval_count": {
                      "type": "integer",
                      "example": 2
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown `scope` or `status`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestError"
                }
              }
            }
          },
          "404": {
            "description": "The Access Requests module is switched off for this tenant"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Request access",
        "description": "Opens an access request for the caller, through the same creation service as the web form.\nPick `target_type` and `target_id` from `GET /service_desk/access_requests/options`.\nWhen the tenant's zero-touch policy covers the target (e.g. a location in a city you already\nwork in, or an open workspace), the request is granted on the spot and comes back `executed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_type",
                  "target_id",
                  "justification"
                ],
                "properties": {
                  "target_type": {
                    "type": "string",
                    "enum": [
                      "Location",
                      "Workspace",
                      "Site",
                      "SupportTeam",
                      "MarketplaceApp",
                      "AssetPro::Category"
                    ]
                  },
                  "target_id": {
                    "type": "integer"
                  },
                  "justification": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 2000
                  },
                  "requested_role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "manager",
                      "co_manager"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Request opened (or granted instantly)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestWriteResponse"
                }
              }
            }
          },
          "422": {
            "description": "Not opened — validation errors (short justification, already a member, a pending request\nalready open for this target, a target in another tenant) or a type that cannot be requested.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/access_requests/options": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "What the caller can request",
        "description": "The request catalog the web New Access Request form shows: one entry per kind of access the\ncaller can meaningfully ask for, each with its pickable targets (up to 200 per kind, by name).\nA kind with nothing to offer is omitted. `clearance: instant` marks kinds that CAN be granted\nwithout approval; `items[].instant` says whether that particular target would be.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The catalog",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "member",
                        "manager",
                        "co_manager"
                      ]
                    },
                    "request_types": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "example": "Location"
                          },
                          "title": {
                            "type": "string",
                            "example": "Work at a Location"
                          },
                          "description": {
                            "type": "string"
                          },
                          "icon": {
                            "type": "string",
                            "example": "fas fa-map-marker-alt"
                          },
                          "color": {
                            "type": "string",
                            "example": "primary"
                          },
                          "clearance": {
                            "type": "string",
                            "enum": [
                              "instant",
                              "approval"
                            ]
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "subtitle": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "instant": {
                                  "type": "boolean",
                                  "description": "Present for Location and Workspace targets"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/access_requests/{id}": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get an access request",
        "description": "The detail page — justification, decision notes, audit trail and the actions the caller may take.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The access request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "access_request": {
                      "$ref": "#/components/schemas/AccessRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not visible to the caller"
          }
        }
      }
    },
    "/service_desk/access_requests/{id}/approve": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Approve an access request",
        "description": "Approves and grants the access in one step. Allowed for a full admin and for the target's\ndesignated approver while the request is pending. If the grant fails, nothing is granted and\nthe request stays pending (422).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessRequestId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "approval_notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/AccessRequestDecided"
          },
          "403": {
            "$ref": "#/components/responses/AccessRequestForbidden"
          },
          "404": {
            "description": "Not found, or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/AccessRequestDecisionFailed"
          }
        }
      }
    },
    "/service_desk/access_requests/{id}/reject": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Reject an access request",
        "description": "Same gate as approve. The requester is notified.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rejection_reason"
                ],
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "description": "Required — the requester is told why; blank is a 422"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/AccessRequestDecided"
          },
          "403": {
            "$ref": "#/components/responses/AccessRequestForbidden"
          },
          "404": {
            "description": "Not found, or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/AccessRequestDecisionFailed"
          }
        }
      }
    },
    "/service_desk/access_requests/{id}/cancel": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Cancel an access request",
        "description": "Allowed for the requester, a full admin or a Service Desk app admin, while the request can still be cancelled.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessRequestId"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/AccessRequestDecided"
          },
          "403": {
            "$ref": "#/components/responses/AccessRequestForbidden"
          },
          "404": {
            "description": "Not found, or not visible to the caller"
          },
          "422": {
            "$ref": "#/components/responses/AccessRequestDecisionFailed"
          }
        }
      }
    },
    "/service_desk/access_requests/{id}/resubmit": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Ask again",
        "description": "Opens a NEW request for the same target from a rejected, cancelled or expired one — the old\nrequest keeps its decision. Only the original requester may resubmit. The justification and\nrole default to the original's. Every creation rule applies again (e.g. already a member,\na pending request already open).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessRequestId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "justification": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 2000
                  },
                  "requested_role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "manager",
                      "co_manager"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new request",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/AccessRequestWriteResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "resubmitted_from_id": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccessRequestForbidden"
          },
          "404": {
            "description": "Not found, or not visible to the caller"
          },
          "422": {
            "description": "Not resubmittable (`code` `not_resubmittable`) or the new request failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/cmdb": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "List configuration items (CMDB)",
        "description": "The tenant's Configuration Management Database — servers, applications, databases, network gear and\nthe services they make up. Same list, filters and counters as the web CMDB page.\n\n**Who:** Service Desk staff — a help desk agent, a Service Desk app admin or a business admin (403\notherwise). `can_manage` says whether the viewer may create and edit CIs (Service Desk admins only).\nAnswers 404 while the tenant has the CMDB module switched off.\n\n`stats` counts the WHOLE CMDB, not the filtered page. `first_run` is true when the CMDB is empty and no\nfilter is applied.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search name, CI number and description",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ci_type",
            "in": "query",
            "required": false,
            "description": "One type, or several as `ci_type[]=`",
            "schema": {
              "type": "string",
              "enum": [
                "service",
                "server",
                "application",
                "database",
                "network",
                "storage",
                "other"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "inactive",
                "planned",
                "retired"
              ]
            }
          },
          {
            "name": "environment",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "production",
                "staging",
                "development",
                "dr"
              ]
            }
          },
          {
            "name": "criticality",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "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": "A page of configuration items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "configuration_items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConfigurationItemSummary"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "active": {
                          "type": "integer"
                        },
                        "inactive": {
                          "type": "integer"
                        },
                        "planned": {
                          "type": "integer"
                        },
                        "retired": {
                          "type": "integer"
                        }
                      }
                    },
                    "filters_applied": {
                      "type": "boolean"
                    },
                    "first_run": {
                      "type": "boolean"
                    },
                    "sample_data_present": {
                      "type": "boolean",
                      "description": "The CMDB holds example (sample-data) CIs"
                    },
                    "can_manage": {
                      "type": "boolean"
                    },
                    "options": {
                      "$ref": "#/components/schemas/ConfigurationItemOptions"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff, or the token lacks read:service_desk"
          },
          "404": {
            "description": "The CMDB module is switched off for this tenant"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Create a configuration item",
        "description": "**Who:** Service Desk admins (a business admin or a Service Desk app admin); 403 otherwise. Needs\nwrite:service_desk. The CI number (`CI-00001`) is generated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "configuration_item"
                ],
                "properties": {
                  "configuration_item": {
                    "$ref": "#/components/schemas/ConfigurationItemInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Configuration item created successfully."
                    },
                    "configuration_item": {
                      "$ref": "#/components/schemas/ConfigurationItemDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a Service Desk admin"
          },
          "422": {
            "description": "Validation failed — `errors` carries the messages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CmdbValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/cmdb/search": {
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Find a configuration item to relate",
        "description": "The \"add relationship\" picker: live (not retired) CIs by name, at most 20. Service Desk staff.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "exclude_ids",
            "in": "query",
            "required": false,
            "description": "CI ids to leave out (e.g. the CI being edited), up to 50",
            "schema": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching CIs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "configuration_items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConfigurationItemRef"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff, or the token lacks read:service_desk"
          }
        }
      }
    },
    "/service_desk/cmdb/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Get a configuration item with its relationships",
        "description": "The CI page: details, upstream relationships (the CIs this one depends on), downstream relationships\n(the CIs that depend on it), the impact counters, and the problems and changes linked to it.\n\n`linked_problems` / `linked_changes` list the 10 most recent with the true `total_count`; each has\n`available: false` and no rows while that module is switched off. `actions` says what the viewer may do\non this CI — Edit, Retire and Add Relationship disappear once it is retired.\n\nService Desk staff; 404 for a CI that is not in this tenant.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The configuration item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "configuration_item": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ConfigurationItemDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "relationships": {
                              "type": "object",
                              "properties": {
                                "total_count": {
                                  "type": "integer"
                                },
                                "upstream": {
                                  "type": "array",
                                  "items": {
                                    "$ref": "#/components/schemas/ConfigurationItemRelationship"
                                  }
                                },
                                "downstream": {
                                  "type": "array",
                                  "items": {
                                    "$ref": "#/components/schemas/ConfigurationItemRelationship"
                                  }
                                }
                              }
                            },
                            "impact": {
                              "type": "object",
                              "properties": {
                                "downstream": {
                                  "type": "integer",
                                  "description": "Live CIs that depend on this one"
                                },
                                "open_problems": {
                                  "type": "integer"
                                },
                                "open_changes": {
                                  "type": "integer"
                                }
                              }
                            },
                            "linked_problems": {
                              "type": "object",
                              "properties": {
                                "available": {
                                  "type": "boolean"
                                },
                                "total_count": {
                                  "type": "integer"
                                },
                                "items": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "problem_number": {
                                        "type": "string"
                                      },
                                      "title": {
                                        "type": "string"
                                      },
                                      "status": {
                                        "type": "string"
                                      },
                                      "priority": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "linked_changes": {
                              "type": "object",
                              "properties": {
                                "available": {
                                  "type": "boolean"
                                },
                                "total_count": {
                                  "type": "integer"
                                },
                                "items": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "change_number": {
                                        "type": "string"
                                      },
                                      "title": {
                                        "type": "string"
                                      },
                                      "change_type": {
                                        "type": "string"
                                      },
                                      "status": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "actions": {
                              "type": "object",
                              "properties": {
                                "edit": {
                                  "type": "boolean"
                                },
                                "retire": {
                                  "type": "boolean"
                                },
                                "add_relationship": {
                                  "type": "boolean"
                                },
                                "remove_relationship": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    "options": {
                      "$ref": "#/components/schemas/ConfigurationItemOptions"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not Service Desk staff"
          },
          "404": {
            "description": "Not found, or the CMDB module is switched off"
          }
        }
      },
      "patch": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Update a configuration item",
        "description": "Service Desk admins only. Send only the fields to change. `attributes_data: {}` clears the custom\nattributes; leaving the key out keeps them.\n\nSetting `status` to `retired` on a CI that is not retired yet retires it as\n`POST /service_desk/cmdb/{id}/retire` does, so that update is delete-level\n(owner decision 2026-10-04, #6721): on top of `write:service_desk` it\nrequires the `destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "configuration_item"
                ],
                "properties": {
                  "configuration_item": {
                    "$ref": "#/components/schemas/ConfigurationItemInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Configuration item updated."
                    },
                    "configuration_item": {
                      "$ref": "#/components/schemas/ConfigurationItemDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a Service Desk admin"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CmdbValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/cmdb/{id}/retire": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Retire a configuration item",
        "description": "Marks the CI as no longer in use (status `retired`). Service Desk admins only.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retired",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Configuration item retired."
                    },
                    "configuration_item": {
                      "$ref": "#/components/schemas/ConfigurationItemDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a Service Desk admin"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Could not be retired"
          }
        }
      }
    },
    "/service_desk/cmdb/{id}/relationships": {
      "post": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Relate another configuration item",
        "description": "Service Desk admins only. `direction: child` makes the related CI a child of this one (it depends on\nthis CI — downstream); `parent` makes it a parent (this CI depends on it — upstream). A relationship\nthat would close a loop of the same type is refused.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "related_ci_id",
                  "direction",
                  "relationship_type"
                ],
                "properties": {
                  "related_ci_id": {
                    "type": "integer"
                  },
                  "direction": {
                    "type": "string",
                    "enum": [
                      "child",
                      "parent"
                    ]
                  },
                  "relationship_type": {
                    "type": "string",
                    "enum": [
                      "depends_on",
                      "runs_on",
                      "part_of",
                      "connects_to",
                      "uses"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Related",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Relationship added."
                    },
                    "direction": {
                      "type": "string",
                      "enum": [
                        "upstream",
                        "downstream"
                      ],
                      "description": "Where the new row appears on this CI"
                    },
                    "relationship": {
                      "$ref": "#/components/schemas/ConfigurationItemRelationship"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a Service Desk admin"
          },
          "404": {
            "description": "This CI, or the related CI, is not in this tenant"
          },
          "422": {
            "description": "Invalid direction, type, duplicate, self-reference or cycle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CmdbValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/service_desk/cmdb/{id}/relationships/{relationship_id}": {
      "delete": {
        "tags": [
          "Service Desk"
        ],
        "summary": "Remove a relationship",
        "description": "Service Desk admins only. 404 when the relationship does not belong to this CI.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "relationship_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Relationship removed."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a Service Desk admin"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/service_desk/grievances": {
      "get": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "List grievances",
        "description": "A handler sees the tenant's queue; everyone else sees the grievances they\nfiled or are assigned. Ordered by stage deadline, then newest filed. The\nstats (total, open, overdue, per-stage) follow search and priority but not\nstage/overdue, as the web page's filter pills do. `priority` is ignored\nwhile the tenant hides priority from this caller.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "stage",
            "in": "query",
            "required": false,
            "description": "A stage key from `stages`",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "Only grievances past their stage deadline",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "description": "One priority, or several as priority[]",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "critical"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Matches title or description",
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of grievances",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "is_handler": {
                      "type": "boolean",
                      "description": "Whether the caller works the whole queue"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "open": {
                          "type": "integer"
                        },
                        "overdue": {
                          "type": "integer"
                        },
                        "by_stage": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "stages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "order": {
                            "type": "integer"
                          },
                          "sla_hours": {
                            "type": "integer",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "per_page": {
                      "type": "integer"
                    },
                    "grievances": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskGrievanceSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      },
      "post": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "File a grievance",
        "description": "Files a grievance for the caller at the tenant's first workflow stage, with\nthat stage's deadline. The tenant's required grievance questions are\nenforced. Assignee, team and department are honoured for handlers only.\n`anonymous: true` is refused (422) — a grievance's filer must be able to\nfollow it; anonymous reporting is Speak Up's. Files go as multipart\n`attachments[]` (10 MB each, within the tenant's per-request limit); files\nthat could not be attached are named in `attachments_warning`. Send an\n`Idempotency-Key` header so a retried Submit does not file twice.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "grievance"
                ],
                "properties": {
                  "grievance": {
                    "$ref": "#/components/schemas/ServiceDeskGrievanceInput"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "grievance": {
                    "$ref": "#/components/schemas/ServiceDeskGrievanceInput"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Filed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Grievance filed successfully."
                    },
                    "attachments_warning": {
                      "type": "string",
                      "nullable": true
                    },
                    "grievance": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "409": {
            "description": "Idempotency conflict — the `Idempotency-Key` was reused with a different body (`idempotency_conflict`), or the first request with it is still running (`idempotency_in_progress`, with `Retry-After`).",
            "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": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/form_options": {
      "get": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "What the filing and edit forms draw",
        "description": "The tenant's grievance questions (the same definitions the web form draws),\nthe priority choices, the first stage and the attachment limits. Handlers\nalso get the team and assignee pickers; pass `grievance_id` when editing so\nthe grievance's own team and assignee are always offered.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "grievance_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Form options",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "priorities": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "default_priority": {
                      "type": "string"
                    },
                    "initial_stage": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
                    },
                    "custom_fields": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "A ServiceDeskCustomField definition (label, field_type, required, options…)"
                      }
                    },
                    "attachments": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "max_per_request": {
                          "type": "integer"
                        },
                        "max_file_bytes": {
                          "type": "integer"
                        }
                      }
                    },
                    "anonymous_allowed": {
                      "type": "boolean",
                      "example": false
                    },
                    "teams": {
                      "type": "array",
                      "description": "Handlers only",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
                      }
                    },
                    "assignees": {
                      "type": "array",
                      "description": "Handlers only",
                      "items": {
                        "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Get a grievance",
        "description": "The stage, its deadline, the stage history, the comments, the answered\nquestions and the documents — what the web page shows this caller. The\nrequester and a non-handler assignee get the stage history as plain\n\"moved from X to Y\" lines and the comment thread without internal notes.\nRecords a \"viewed\" audit entry.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The grievance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "grievance": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      },
      "patch": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Edit a grievance (handlers)",
        "description": "`custom_fields` merge over the stored answers, so an answer under a\nquestion an admin has since switched off is kept. The audit entry names\nthe changed fields, never their values.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "grievance"
                ],
                "properties": {
                  "grievance": {
                    "$ref": "#/components/schemas/ServiceDeskGrievanceInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ServiceDeskGrievanceUpdated"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}/advance_stage": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Advance to a later stage (handlers)",
        "description": "`stage` must be one of the grievance's `allowed_transitions` (the next\nstage, Resolved, or Closed). A stage with a named approver or approver\ngroup can only be entered by them or an admin. Sets the new stage's\ndeadline and tells the grievant when the tenant has that switched on.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stage"
                ],
                "properties": {
                  "stage": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Staff-only note recorded on the stage history"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ServiceDeskGrievanceUpdated"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}/revert_stage": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Move back to an earlier stage (admins)",
        "description": "Business admins and Service Desk admins only. `reason` is required and recorded on the grievance.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stage",
                  "reason"
                ],
                "properties": {
                  "stage": {
                    "type": "string",
                    "description": "One of the grievance's `revert_targets`"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ServiceDeskGrievanceUpdated"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}/comments": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Comment on a grievance",
        "description": "Anyone who can read the grievance. `internal: true` (hidden from the\nrequester) is for handlers only: anyone else sending it is REFUSED with\n403 rather than having the note published to the grievant. Send an\n`Idempotency-Key` header so a retried Post does not comment twice — a\nretry with the same key and body replays the first 201.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string"
                  },
                  "internal": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "comment": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceComment"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "409": {
            "description": "Idempotency conflict — the `Idempotency-Key` was reused with a different body (`idempotency_conflict`), or the first request with it is still running (`idempotency_in_progress`, with `Retry-After`).",
            "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": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}/documents": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Add documents",
        "description": "Handlers, or the grievant on their own grievance, up to the tenant's per-grievance file limit (10 MB a file).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "documents": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ServiceDeskGrievanceUpdated"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "422": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/service_desk/grievances/{id}/documents/{attachment_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "attachment_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Service Desk Grievances"
        ],
        "summary": "Remove a document (admins)",
        "description": "Business admins and Service Desk admins only — the file is deleted permanently.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:service_desk` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/ServiceDeskGrievanceUpdated"
          },
          "403": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          },
          "404": {
            "$ref": "#/components/responses/ServiceDeskGrievanceError"
          }
        }
      }
    },
    "/desktop/bootstrap": {
      "get": {
        "tags": [
          "Desktop"
        ],
        "summary": "Everything the desktop app needs to draw itself, in one answer",
        "description": "Call once right after login (and again when connectivity returns if the\nfirst call never answered). Composed from the SAME builders the per-app\nendpoints render — `apps.chat` is the body of `GET /chat/config`,\n`apps.recorder` of `GET /recorder/config`, `apps.ai_notepad.config` /\n`.preferences` of the two AI Notepad GETs, `apps.ask_ai` of\n`GET /ask_ai/settings` (minus `success`), `branding` of\n`GET /branding/theme` — so a client that already speaks those endpoints\nfeeds the blocks to the same code.\n\n**Never 403s.** A tenant with no desktop app at all answers 200 with an\nempty `rail.landing`; that is the answer the client draws its\n\"nothing is switched on for the desktop app yet\" shell from. Per-app\nscopes are REPORTED (`denial_reason: insufficient_scope`), never\nenforced here — the per-app endpoints enforce them on use.\n\n**`rail.apps`** follows the company's order set under General Settings\n→ OneMango Desktop. The default is `chat`, `ask_ai`, `ai_notepad`,\n`recorder`, `people`. Reordering does not change access. Each slot carries `enabled` (the\ntenant has the app), `accessible` (this person, with this token, may\nopen it) and `denial_reason` — `app_not_enabled`, `app_not_accessible`\n(outside the app's audience / not yet published), `upgrade_required`\n(Ask AI below the Standard tier), `insufficient_scope`, `unavailable`\n(the app is the person's but its boot block failed to build — the\nrest of the answer stands; ask again later), or `null`.\n`people` is the Chat app's directory column and rides Chat's gate.\n`rail.landing` is the first accessible slot, or `null`.\n\n`bootstrap_version` is bumped only when a client would need new code\nto read the answer; additive keys never bump it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The desktop boot answer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "bootstrap_version",
                    "generated_at",
                    "user",
                    "business",
                    "rail",
                    "apps",
                    "branding",
                    "realtime"
                  ],
                  "properties": {
                    "bootstrap_version": {
                      "type": "integer",
                      "example": 1
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "first_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "last_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "email": {
                          "type": "string"
                        },
                        "avatar_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "time_zone": {
                          "type": "string",
                          "nullable": true,
                          "description": "The zone the caller reads — their own choice, else their\nplace's, else the company's — as stored: usually a Rails\nname (\"Central Time (US & Canada)\"), sometimes an IANA id.\n",
                          "example": "Central Time (US & Canada)"
                        },
                        "time_zone_iana": {
                          "type": "string",
                          "nullable": true,
                          "description": "The same zone as a canonical IANA id, for Intl and\nmoment-timezone, which know no Rails name. Null only when\nthe zone can't be resolved. Canonical: a zone tzdata merged\nwith another of the same clock answers that one\n(\"Copenhagen\" is \"Europe/Berlin\"), so compute times with\nit rather than printing it as a place.\n",
                          "example": "America/Chicago"
                        },
                        "time_zone_set": {
                          "type": "boolean",
                          "description": "Whether the caller chose a zone themselves (`false` — `time_zone` follows their place or the company)."
                        },
                        "language": {
                          "type": "string",
                          "description": "The language content reaches the caller in — their own\nsetting, else their place's, else the company default.\nNever null since Knowledge Layer P4 (was the stored\nsetting, null when unset).\n",
                          "example": "en"
                        },
                        "language_set": {
                          "type": "boolean",
                          "description": "Whether the caller chose a language themselves (`false` is the profile's \"Not set\")."
                        },
                        "admin": {
                          "type": "boolean",
                          "description": "Whether the person can enable apps themselves (the Apps Marketplace is an admin surface)."
                        }
                      }
                    },
                    "business": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "subdomain": {
                          "type": "string"
                        },
                        "time_zone": {
                          "type": "string"
                        },
                        "week_start_day": {
                          "type": "string",
                          "example": "monday"
                        },
                        "logo_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "apps_marketplace_path": {
                          "type": "string",
                          "example": "/admin/app_marketplace",
                          "description": "Where an admin turns a desktop app on. A path — build it against the host you signed in to."
                        }
                      }
                    },
                    "rail": {
                      "type": "object",
                      "properties": {
                        "landing": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "chat",
                            "ask_ai",
                            "ai_notepad",
                            "recorder",
                            "people"
                          ]
                        },
                        "apps": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string",
                                "enum": [
                                  "chat",
                                  "ask_ai",
                                  "ai_notepad",
                                  "recorder",
                                  "people"
                                ]
                              },
                              "slug": {
                                "type": "string",
                                "description": "The marketplace app the slot belongs to."
                              },
                              "enabled": {
                                "type": "boolean"
                              },
                              "accessible": {
                                "type": "boolean"
                              },
                              "denial_reason": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                  "app_not_enabled",
                                  "app_not_accessible",
                                  "upgrade_required",
                                  "insufficient_scope",
                                  "unavailable"
                                ]
                              }
                            }
                          }
                        }
                      }
                    },
                    "apps": {
                      "type": "object",
                      "description": "Each accessible app's boot block; `null` for a slot the person may not open.",
                      "properties": {
                        "chat": {
                          "type": "object",
                          "nullable": true,
                          "description": "The body of GET /chat/config."
                        },
                        "ask_ai": {
                          "type": "object",
                          "nullable": true,
                          "description": "The body of GET /ask_ai/settings without `success`."
                        },
                        "ai_notepad": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "config": {
                              "type": "object",
                              "description": "The `ai_notepad` block of GET /ai_notepad/config."
                            },
                            "preferences": {
                              "type": "object",
                              "description": "The body of GET /ai_notepad/preferences."
                            }
                          }
                        },
                        "recorder": {
                          "type": "object",
                          "nullable": true,
                          "description": "The `recorder` block of GET /recorder/config."
                        }
                      }
                    },
                    "branding": {
                      "type": "object",
                      "required": [
                        "theme",
                        "business_id"
                      ],
                      "properties": {
                        "theme": {
                          "type": "object",
                          "required": [
                            "tokens",
                            "branding",
                            "brand_fonts",
                            "palette"
                          ],
                          "properties": {
                            "tokens": {
                              "type": "object",
                              "description": "Every theme token, defaults overlaid with the tenant's saved values. Additive over time — treat unknown keys as forward compatibility.",
                              "additionalProperties": true,
                              "properties": {
                                "brand_primary": {
                                  "type": "string",
                                  "example": "#3066b3"
                                },
                                "font_heading": {
                                  "type": "string"
                                },
                                "font_body": {
                                  "type": "string"
                                },
                                "font_size_base": {
                                  "type": "string",
                                  "example": "15px"
                                },
                                "text_primary": {
                                  "type": "string"
                                },
                                "text_muted": {
                                  "type": "string"
                                },
                                "heading_color": {
                                  "type": "string"
                                },
                                "page_bg": {
                                  "type": "string"
                                },
                                "card_bg": {
                                  "type": "string"
                                },
                                "modal_bg": {
                                  "type": "string"
                                },
                                "navbar_bg": {
                                  "type": "string"
                                },
                                "navbar_text": {
                                  "type": "string",
                                  "description": "Derived from `navbar_bg` when the admin left it unset."
                                },
                                "sidebar_text": {
                                  "type": "string"
                                },
                                "sidebar_rail_bg": {
                                  "type": "string"
                                },
                                "sidebar_rail_active_bg": {
                                  "type": "string"
                                },
                                "border_color": {
                                  "type": "string"
                                },
                                "border_radius": {
                                  "type": "string"
                                },
                                "shadow_color": {
                                  "type": "string"
                                },
                                "style_family": {
                                  "type": "string",
                                  "description": "The style family, which owns corner radius, card style and density."
                                }
                              }
                            },
                            "branding": {
                              "type": "object",
                              "properties": {
                                "logo_url": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "favicon_url": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "sidebar_logo_bg_color": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            },
                            "custom_css": {
                              "type": "string",
                              "nullable": true,
                              "description": "The tenant's raw custom stylesheet, kept apart from the tokens because it is not a CSS variable."
                            },
                            "brand_fonts": {
                              "type": "array",
                              "description": "The uploaded brand fonts the font tokens name, with their files' URLs on this origin. Empty when the tokens name none.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "css_family": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "category": {
                                    "type": "string"
                                  },
                                  "faces": {
                                    "type": "array",
                                    "items": {
                                      "type": "object",
                                      "properties": {
                                        "url": {
                                          "type": "string"
                                        },
                                        "weight": {
                                          "type": "integer",
                                          "example": 400
                                        },
                                        "style": {
                                          "type": "string",
                                          "enum": [
                                            "normal",
                                            "italic"
                                          ]
                                        },
                                        "format": {
                                          "type": "string"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "palette": {
                              "type": "array",
                              "description": "Every colour the company's colour choosers offer — brand, accent, the colours it added and the ten named ones — each with the fill and the text colour that reads on it, and the colour as a mark on a card in the light and dark themes. A record that stores a colour stores its `key`.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "key": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "group": {
                                    "type": "string"
                                  },
                                  "color": {
                                    "type": "string"
                                  },
                                  "fill": {
                                    "type": "string"
                                  },
                                  "ink": {
                                    "type": "string"
                                  },
                                  "mark": {
                                    "type": "string"
                                  },
                                  "mark_dark": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the theme (or, when it was never saved, the business) last changed."
                        }
                      }
                    },
                    "realtime": {
                      "type": "object",
                      "description": "The Pusher bootstrap (key, cluster, user_id, business_id) — the `pusher` block of GET /chat/config, present whether or not the tenant has Chat, because the Recorder and AI Notepad inbox channels authorise at their own endpoints.",
                      "properties": {
                        "pusher": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/branding/theme": {
      "get": {
        "tags": [
          "Branding"
        ],
        "summary": "The business's theme tokens, logo, fonts and palette",
        "description": "The values an admin sets on Theme Settings, resolved for the caller's\nbusiness, so a web or desktop client can set its CSS custom properties\nat runtime and re-theme per tenant without a rebuild. A business that\nnever saved a theme gets the full default token set. Read-only; carries\nno secrets.\n\nEach key under `theme.tokens` maps one-to-one to a CSS custom property\nnamed `--{key-dasherized}` (`brand_primary` → `--brand-primary`), the\nsame mapping the server's own stylesheet uses.\n\nThe answer carries an `ETag` and `Last-Modified`; send them back\n(`If-None-Match` / `If-Modified-Since`) and an unchanged theme answers\n304 with no body. A new logo, favicon or brand font changes the\nvalidators even when the theme row did not change.\n\n**Token scopes:** none — any signed-in token may read its own\nbusiness's theme.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "The `ETag` of the copy the client holds.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Modified-Since",
            "in": "header",
            "required": false,
            "description": "The `Last-Modified` of the copy the client holds.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The theme.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandingTheme"
                }
              }
            }
          },
          "304": {
            "description": "The client's copy is current."
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tasks/config": {
      "get": {
        "tags": [
          "Tasks"
        ],
        "summary": "What the create-task form may offer this person",
        "description": "Always 200 for a signed-in person, so a client hides its \"Create a\ntask\" control instead of offering one that is refused on submit.\n`enabled` says the Tasks app is open to the caller; `can_create` adds\nthe organization's self-assignment rule (managers and Tasks admins\nalways, everyone else while self-assignment is allowed).\n\n**Token scopes:** `write:tasks` (satisfied by `write:own_tasks`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The form's options.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tasks"
                  ],
                  "properties": {
                    "tasks": {
                      "type": "object",
                      "required": [
                        "enabled",
                        "can_create",
                        "priority_enabled",
                        "priorities",
                        "categories"
                      ],
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "can_create": {
                          "type": "boolean"
                        },
                        "priority_enabled": {
                          "type": "boolean",
                          "description": "The organization's priority system is on. When off, every task is stored as `normal`."
                        },
                        "priorities": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "normal",
                              "important",
                              "critical"
                            ]
                          }
                        },
                        "categories": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "Work",
                              "Personal",
                              "Project",
                              "Meeting",
                              "Follow-up"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` — \"Required scope: write:tasks\".",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tasks": {
      "post": {
        "tags": [
          "Tasks"
        ],
        "summary": "Create a task assigned to the caller",
        "description": "Creates a simple, pending My Task assigned to and by the caller. No\nnotification is sent — the task is the caller's own.\n\nA `source` naming a chat message is recorded on the task only when the\ncaller can still see that message (a room of this business they are a\nmember of, the message not deleted). When it cannot be found the task\nis saved without the link and `warnings` says so.\n\nAn `Idempotency-Key` retry replays the first 201 instead of writing a\nsecond task.\n\n**Token scopes:** `write:tasks` (satisfied by `write:own_tasks`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A key you choose per create; a retry with the same key replays the first answer.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "description": "Each field must be a single value; a list or an object reads as absent.",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Trimmed and cut to 255 characters (then named in `truncated_fields`)."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 10000,
                    "description": "Cut to 10,000 characters (then named in `truncated_fields`)."
                  },
                  "due_on": {
                    "type": "string",
                    "format": "date",
                    "description": "YYYY-MM-DD, read in the caller's time zone. Alone, the task is due any time that day."
                  },
                  "due_time": {
                    "type": "string",
                    "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                    "description": "HH:MM, with `due_on` — the task is due at that minute."
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "normal",
                      "important",
                      "critical"
                    ],
                    "default": "normal",
                    "description": "Stored as `normal` while the organization's priority system is off."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "Work",
                      "Personal",
                      "Project",
                      "Meeting",
                      "Follow-up"
                    ],
                    "description": "Blank stores `general`."
                  },
                  "source": {
                    "type": "object",
                    "description": "Where the task came from.",
                    "required": [
                      "type"
                    ],
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "chat_message"
                        ]
                      },
                      "room_id": {
                        "type": "integer"
                      },
                      "message_id": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The task was created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "priority": {
                          "type": "string"
                        },
                        "category": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "due_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "due_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "url_in_app": {
                          "type": "string",
                          "description": "The task's page in the web app, a path on the tenant's host."
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Present when the source message could not be linked.",
                      "items": {
                        "type": "string"
                      }
                    },
                    "truncated_fields": {
                      "type": "array",
                      "description": "Present when `title` or `description` was cut.",
                      "items": {
                        "type": "string",
                        "enum": [
                          "title",
                          "description"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`missing_params` — \"Title is required\".",
            "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": "`insufficient_permissions` (\"Required scope: write:tasks\"), `tasks_not_enabled` (the Tasks app is not open to the caller) or `self_assignment_disabled` (creating your own tasks is turned off for the organization).",
            "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": "`idempotency_conflict` — this Idempotency-Key was used for a different 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"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`invalid_priority`, `invalid_category`, `invalid_due_date` (not YYYY-MM-DD and HH:MM, or a year outside 1000–9999), `invalid_source` (a source type other than `chat_message`) or `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"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`task_create_failed` — no task was written; the error is reported.",
            "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": "`task_create_unavailable` — a passing infrastructure failure; no task was written. Try again.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/users": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List people in the caller's business",
        "description": "The people directory as a page of rows: HRIS syncs, the mobile roster,\npartner integrations and One Mango Desktop's People tab all read it.\nService accounts and AI-agent principals are never listed.\n\n**Who sees what.** An admin, and a machine token with no person behind\nit (an HRIS sync an admin minted), get every field and may ask for\ndeactivated (`active`) and former (`include_deleted`) members. Everyone\nelse sees active members only, with each field the directory hides from\nthem answered as null (an empty list for `locations`) — and a search\narm, filter or sort on a hidden field is ignored rather than applied.\n\nEach row carries the person's chat presence, so a client draws live\ndots without a second call.\n\n**Token scopes:** `read:people_directory` (`read:users` implies it).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, 1 to 10,000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Matches the full name, name, preferred name or employee UID, and the email address where the caller's directory shows email.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "department",
            "in": "query",
            "required": false,
            "description": "A department name to match (contains, case-insensitive). Returns the people in every matching department of this organization and in the departments beneath it. Applied only where the directory shows department.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "employment_status",
            "in": "query",
            "required": false,
            "description": "An exact employment status. Applied only where the directory shows it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updated_after",
            "in": "query",
            "required": false,
            "description": "Only people whose record changed after this time. An unreadable value is ignored.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "active",
            "in": "query",
            "required": false,
            "description": "Full view only — `true` for active members, `false` for deactivated ones. Others always get active members.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "include_deleted",
            "in": "query",
            "required": false,
            "description": "Full view only — include people who have left the business.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "required": false,
            "description": "`email` and `hire_date` sort by name where the directory hides them; any other value sorts by name.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "email",
                "hire_date",
                "created_at",
                "updated_at"
              ],
              "default": "name"
            }
          },
          {
            "name": "sort_order",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of people.",
            "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",
                  "required": [
                    "users",
                    "meta"
                  ],
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UsersDirectoryRow"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_integer_format` or `invalid_integer_value` (`page` or `per_page` is not a positive integer), `page_limit_exceeded` (over 10,000) or `per_page_limit_exceeded` (over 100).",
            "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": "`insufficient_permissions` — \"Required scope: read:people_directory\".",
            "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/shared": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "List shared conversations",
        "description": "Requires read:shared_ai_conversations, active Ask AI access and explicit membership in this business. Shared conversations are separate from personal AI history and memory. Responses are private and non-cacheable.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "invited"
                            ]
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "owner",
                              "contributor",
                              "viewer"
                            ]
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Confirm a reviewed sharing preview",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Explicit confirmation is required. Repeating the same valid preview returns the same shared continuation; it never publishes the original private transcript.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preview_token": {
                    "type": "string"
                  },
                  "confirmed": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "preview_token",
                  "confirmed"
                ]
              }
            }
          }
        }
      }
    },
    "/ask_ai/shared/people": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Find eligible coworkers",
        "description": "Requires read:shared_ai_conversations, active Ask AI access and explicit membership in this business. Shared conversations are separate from personal AI history and memory. Responses are private and non-cacheable. Returns up to 50 active coworkers with Ask AI access.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "text": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ]
      }
    },
    "/ask_ai/shared/preview": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Preview a reviewed private-conversation summary",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. The caller must own the private source. Only the explicitly reviewed summary is copied. Attachments, hidden prompts, memory, tool traces and unreviewed assistant answers are excluded. The encrypted preview expires after 15 minutes and is bound to this actor, business and source revision. At most seven coworkers can be invited.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preview": {
                      "type": "object"
                    },
                    "preview_token": {
                      "type": "string"
                    },
                    "exclusions": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "summary": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 12000
                  },
                  "participant_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "minItems": 1,
                    "maxItems": 7,
                    "uniqueItems": true
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "contributor"
                    ]
                  }
                },
                "required": [
                  "source_id",
                  "title",
                  "summary",
                  "participant_ids",
                  "role"
                ]
              }
            }
          }
        }
      }
    },
    "/ask_ai/shared/{id}": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Read shared messages or an invitation",
        "description": "Requires read:shared_ai_conversations, active Ask AI access and explicit membership in this business. Shared conversations are separate from personal AI history and memory. Responses are private and non-cacheable. Pending invitees receive only conversation title, role, status and invitation:true. Accepted participants receive up to 100 currently visible messages, ordered by sequence. Source-derived content is rechecked against every active participant; unavailable sources are withheld. Search excludes withheld content. The next_before cursor retrieves older messages and is null when no older message is visible to the caller. The opaque version supports polling. Private refusal guidance is visible only to its sender.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "conversation": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "title": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "active",
                                "invited"
                              ]
                            },
                            "role": {
                              "type": "string",
                              "enum": [
                                "owner",
                                "contributor",
                                "viewer"
                              ]
                            },
                            "updated_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "url": {
                              "type": "string"
                            }
                          }
                        },
                        "messages": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "sequence": {
                                "type": "integer"
                              },
                              "role": {
                                "type": "string",
                                "enum": [
                                  "user",
                                  "assistant"
                                ]
                              },
                              "content": {
                                "type": "string"
                              },
                              "author_id": {
                                "type": "integer"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "private": {
                                "type": "boolean"
                              },
                              "unavailable": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "participants": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "role": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              },
                              "available": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "version": {
                          "type": "string"
                        },
                        "permissions": {
                          "type": "object",
                          "properties": {
                            "can_send": {
                              "type": "boolean"
                            }
                          }
                        },
                        "turns": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "actor_id": {
                                "type": "integer"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "queued",
                                  "processing",
                                  "failed",
                                  "cancelled",
                                  "private"
                                ]
                              },
                              "reason": {
                                "type": "string",
                                "nullable": true
                              },
                              "can_retry": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "next_before": {
                          "type": "integer",
                          "nullable": true
                        },
                        "page_limit": {
                          "type": "integer",
                          "enum": [
                            100
                          ]
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "conversation": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "title": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "active",
                                "invited"
                              ]
                            },
                            "role": {
                              "type": "string",
                              "enum": [
                                "owner",
                                "contributor",
                                "viewer"
                              ]
                            },
                            "updated_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "url": {
                              "type": "string"
                            }
                          }
                        },
                        "invitation": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/ask_ai/shared/{id}/accept": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Accept a pending invitation",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Only the named invitee may accept. Transcript access begins after acceptance.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/ask_ai/shared/{id}/decline": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Decline a pending invitation",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Only the named invitee may decline. The conversation leaves the invitee's list, and the owner sees the invitation as declined; the owner may invite the person again.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "declined"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/ask_ai/shared/{id}/participants": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Read participant management details",
        "description": "Requires read:shared_ai_conversations, active Ask AI access and explicit membership in this business. Shared conversations are separate from personal AI history and memory. Responses are private and non-cacheable. Owner only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "available": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Invite, revoke or change a participant role",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Owner only. The owner cannot revoke or alter their own role here. Revocation blocks subsequent reads, search, exports and generation. Re-invitation requires acceptance again. Already copied or exported content cannot be recalled.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "participant_action": {
                    "type": "string",
                    "enum": [
                      "invite",
                      "revoke",
                      "role"
                    ]
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "contributor"
                    ],
                    "description": "Required for invite or role."
                  }
                },
                "required": [
                  "user_id",
                  "participant_action"
                ]
              }
            }
          }
        }
      }
    },
    "/ask_ai/shared/{id}/messages": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Queue a shared AI reply",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Owner or contributor only. Request UUIDs are idempotent per conversation and bound to the sender. Limits: eight pending turns per conversation and 30 messages per sender per hour, plus the business AI budget. Turns execute in order under the actual sender. The first source adapter supports a Service Desk ticket only when every active participant can read it. Other private sources and actions require a private conversation. Permissions and source revisions are rechecked before persisting an answer. Poll the conversation for progress.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "202": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "turn": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "actor_id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "processing",
                            "failed",
                            "cancelled",
                            "private"
                          ]
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "can_retry": {
                          "type": "boolean"
                        }
                      },
                      "nullable": true
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 8000
                  },
                  "request_key": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "ticket_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Optional Service Desk source ticket."
                  }
                },
                "required": [
                  "message",
                  "request_key"
                ]
              }
            }
          }
        }
      }
    },
    "/ask_ai/shared/{id}/turns/{turn_id}/retry": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Retry the sender’s failed reply",
        "description": "Requires write:shared_ai_conversations and active Ask AI access. All person and conversation identifiers are business-scoped. Only the original sender with current contributor access can retry a failed turn. Queued or processing turns cannot be retried.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "202": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "turn": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "actor_id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "processing",
                            "failed",
                            "cancelled",
                            "private"
                          ]
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "can_retry": {
                          "type": "boolean"
                        }
                      },
                      "nullable": true
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "turn_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/ask_ai/shared/{id}/export": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Export the current visible message page",
        "description": "Requires read:shared_ai_conversations, active Ask AI access and explicit membership in this business. Shared conversations are separate from personal AI history and memory. Responses are private and non-cacheable. Downloads JSON containing the same permission-filtered projection as the read endpoint. This is a bounded page, not an unbounded transcript export.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "invited"
                          ]
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "owner",
                            "contributor",
                            "viewer"
                          ]
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sequence": {
                            "type": "integer"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "user",
                              "assistant"
                            ]
                          },
                          "content": {
                            "type": "string"
                          },
                          "author_id": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "private": {
                            "type": "boolean"
                          },
                          "unavailable": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "participants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "available": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "version": {
                      "type": "string"
                    },
                    "permissions": {
                      "type": "object",
                      "properties": {
                        "can_send": {
                          "type": "boolean"
                        }
                      }
                    },
                    "turns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "actor_id": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "queued",
                              "processing",
                              "failed",
                              "cancelled",
                              "private"
                            ]
                          },
                          "reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "can_retry": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "next_before": {
                      "type": "integer",
                      "nullable": true
                    },
                    "page_limit": {
                      "type": "integer",
                      "enum": [
                        100
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/ask_ai/attachments": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Upload private files for a message",
        "description": "Requires write:chat (or write:own_chat). Up to three files; images/documents\nare limited to 8 MB each. One MP4/MOV/WebM video may be up to 100 MB and\n120 seconds, H.264/HEVC/VP8/VP9, at most 4K. Video processing is asynchronous.\nPoll status_url; do not submit its signed_id until processing.state is ready.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "files"
                ],
                "properties": {
                  "files": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload accepted; video evidence is still processing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AskAiAttachment"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Unsupported",
            "corrupt": null,
            "over-limit file or exhausted processing budget": null
          },
          "403": {
            "description": "Ask AI access or token scope denied"
          }
        }
      }
    },
    "/ask_ai/attachments/{signed_id}": {
      "parameters": [
        {
          "in": "path",
          "name": "signed_id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Read private video processing progress",
        "description": "Requires read:chat (or read:own_chat). Owner and tenant are rechecked. Response never includes transcript or frame evidence; Cache-Control is no-store.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current processing status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiAttachmentResponse"
                }
              }
            }
          },
          "404": {
            "description": "Attachment is unavailable to this caller"
          }
        }
      },
      "patch": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Retry interrupted or failed video processing",
        "description": "Requires write:chat (or write:own_chat). Idempotent while queued, processing, or ready. Maximum three attempts per clip and six starts per user/hour. Clips expire after 24 hours; interrupted work can retry after 10 minutes without progress.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "202": {
            "description": "Processing resumed or already active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiAttachmentResponse"
                }
              }
            }
          },
          "404": {
            "description": "Attachment is unavailable to this caller"
          },
          "422": {
            "description": "Expired attachment or processing budget exhausted"
          }
        }
      },
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Cancel video processing and remove extracted evidence",
        "description": "Requires write:chat (or write:own_chat). Cancels the current generation atomically; an in-flight worker cannot restore cancelled evidence. Original private upload expires after 24 hours.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Processing cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiAttachmentResponse"
                }
              }
            }
          },
          "404": {
            "description": "Attachment is unavailable to this caller"
          }
        }
      }
    },
    "/feeds/{id}/feature": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read a post's editorial featuring configuration",
        "description": "Requires read:news_feed_editorial_features and the tenant's editorial\nauthority for this post. The employee feed exposes only the currently\napplicable featured badge; it does not expose this editor configuration.\n\nSince Knowledge Layer Phase 3 (E7) a feature is a Top of News booking on\nthe one placement engine. These endpoints keep their request and\nresponse shapes, but `feature.id` is the booking's (placement's) id — the\nid `POST /feeds/{id}/placements/{placement_id}/end_now` takes.\n",
        "responses": {
          "200": {
            "description": "Current configuration, or null when withdrawn.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feature": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "The Top of News booking's (placement's) id."
                        },
                        "audience_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "starts_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "ends_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "position": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing scope",
            "app access or editorial authority.": null
          },
          "404": {
            "description": "Post unavailable in this business."
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Set a timed editorial feature for a published post",
        "description": "Requires write:news_feed_editorial_features and editorial authority.\nReplaces the current schedule atomically while preserving history.\nThe optional audience narrows the post's existing visibility. This\nplacement does not create a personal pin, acknowledgment or notification.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "editorial_feature"
                ],
                "properties": {
                  "editorial_feature": {
                    "type": "object",
                    "required": [
                      "starts_at",
                      "ends_at",
                      "position"
                    ],
                    "properties": {
                      "audience_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "starts_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "ends_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "position": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 1000
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved feature configuration (`id` is the booking's id).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feature": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "The Top of News booking's (placement's) id."
                        },
                        "audience_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "starts_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "ends_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "position": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing scope",
            "app access or editorial authority.": null
          },
          "404": {
            "description": "Post unavailable in this business."
          },
          "422": {
            "description": "Invalid audience",
            "schedule": null,
            "position or source publication state.": null
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Stop featuring a post",
        "description": "Requires write:news_feed_editorial_features and editorial authority. History and personal pins are preserved.",
        "responses": {
          "200": {
            "description": "Feature withdrawn",
            "including an already-withdrawn request.": null
          },
          "403": {
            "description": "Missing scope",
            "app access or editorial authority.": null
          },
          "404": {
            "description": "Post unavailable in this business."
          }
        }
      }
    },
    "/feeds/{id}/placements/{placement_id}/dismiss": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "description": "Feed post ID",
          "schema": {
            "type": "integer"
          }
        },
        {
          "in": "path",
          "name": "placement_id",
          "required": true,
          "description": "The placement's `id` (see `CommsPlacement`).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Dismiss one of this post's placements for the caller",
        "description": "The native twin of the web card's Dismiss for a placed POST — Top of\nHome, a store's or department's pin, a work screen. Since Knowledge\nLayer Phase 3 (E7) the one placement engine holds feed posts as well as\nbroadcasts. Records the dismissal for the caller only. Only a live\nplacement the caller is in the audience of; anything else is 404 (the id\nis guessable and dismissals feed Measure). A broadcast's placement is\ndismissed through `POST /broadcasts/{id}/placements/{placement_id}/dismiss`.\n\nAsks the `write:news_feed` scope (#6720), before the post is looked up.\n",
        "responses": {
          "200": {
            "description": "Dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dismissed": {
                      "type": "boolean",
                      "example": true
                    },
                    "placement_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `write:news_feed`."
          },
          "404": {
            "description": "`{ error: { code: \"not_found\", message: \"Placement not found\" } }` — no such post or live placement, or the caller is not in its audience."
          }
        }
      }
    },
    "/feeds/{id}/placements/{placement_id}/end_now": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "description": "Feed post ID",
          "schema": {
            "type": "integer"
          }
        },
        {
          "in": "path",
          "name": "placement_id",
          "required": true,
          "description": "The placement's `id`.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "End one of this post's placements now (\"End now\")",
        "description": "Takes the placement down for everyone (Knowledge Layer E7): the author,\nor someone the booking rule lets place it (a Communications admin whose\nreach covers its audience; a store manager for their store's pin). On a\nqueued place (Top of Home) the booking waiting right behind it starts\nnow — `next_placement_id` names it. The broadcast twin is\n`POST /broadcasts/{id}/placements/{placement_id}/end_now`.\n\nEnding a Top of News booking (`news.top`) un-features the post, so it\nneeds `write:news_feed_editorial_features` — the scope\n`DELETE /feeds/{id}/feature` takes. Ending one on any other slot needs\n`write:news_feed` (#6720).\n",
        "responses": {
          "200": {
            "description": "Ended",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The answer of an \"End now\" (`…/placements/{placement_id}/end_now`).",
                  "properties": {
                    "ended": {
                      "type": "boolean",
                      "example": true
                    },
                    "placement_id": {
                      "type": "integer"
                    },
                    "next_placement_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The booking queued behind it that starts now (queued places such as Top of Home only), else null."
                    },
                    "message": {
                      "type": "string",
                      "example": "Ended — it's off Top of Home now. “Holiday hours” starts there now."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`{ error }` — the caller may not end this placement (the refusal's own words), or `insufficient_permissions` when the token lacks the slot's scope (above)."
          },
          "404": {
            "description": "`{ error: { code: \"not_found\", message: \"Placement not found\" } }` — no such post or current placement."
          },
          "422": {
            "description": "`{ error }` — e.g. \"It has already ended.\""
          }
        }
      }
    },
    "/comments/{id}/report": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read your own comment report outcomes",
        "description": "Requires read:news_feed_reports and access to the parent post. Returns\nonly the caller's reports, including safe review outcomes after the\ncomment is removed. Other reporters and moderator notes are excluded.\n",
        "responses": {
          "200": {
            "description": "Up to 25 reports, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reports": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "comment_id": {
                            "type": "integer"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "reported_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "review_status": {
                            "type": "string",
                            "enum": [
                              "pending_review",
                              "approved",
                              "rejected"
                            ]
                          },
                          "reviewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing scope",
            "disabled discussion layer or unavailable post.": null
          },
          "404": {
            "description": "Comment unavailable in this business."
          },
          "422": {
            "description": "`not_published` — the post is a draft or scheduled, asked by someone preparing it"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Report a visible comment for moderation",
        "description": "Requires write:news_feed_reports. Reports by the same caller for the\nsame reason are idempotent. Reports share a pending moderation case;\nsource content stays visible until an authorized moderator decides.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "inappropriate",
                      "spam",
                      "harassment",
                      "misinformation",
                      "other"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report recorded; response includes status and the caller's safe report outcome."
          },
          "403": {
            "description": "Missing scope",
            "disabled discussion layer or unavailable post.": null
          },
          "404": {
            "description": "Comment unavailable in this business."
          },
          "422": {
            "description": "Invalid reason or source no longer available; or `not_published` — the post is a draft or scheduled, asked by someone preparing it."
          }
        }
      }
    },
    "/ask_ai/saved_answers": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "List my saved Ask AI answers",
        "description": "Lists this account's saved answers in the current business, newest saved first.\nRequires `read:chat`, Ask AI app access and Platform Standard or higher.\nReturns at most 30 rows. Pass `next_before` as `before` for the next page;\na null cursor means there are no more rows. An optional conversation filter\nis applied before pagination and only admits this account's private general threads.\n\nThese are the same account-owned Platform bookmarks shown in My Stuff and\nPersonal Agent. Each target is checked again on read. A cleared, missing or\nno-longer-accessible answer returns only `id`, `saved_at` and `available: false`;\nits owner may still remove the bookmark. Transcript text is never copied into\na bookmark. The normal `_meta` and `unread_notification_count` fields are additive.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 1
            },
            "description": "Exclusive saved-answer bookmark id from the previous page's next_before."
          },
          {
            "name": "conversation_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Private conversation session id, the id returned by the conversations API."
          }
        ],
        "responses": {
          "200": {
            "description": "Saved answers, including unavailable placeholders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "saved_answers",
                    "next_before"
                  ],
                  "properties": {
                    "saved_answers": {
                      "type": "array",
                      "maxItems": 30,
                      "items": {
                        "$ref": "#/components/schemas/AskAiSavedAnswer"
                      }
                    },
                    "next_before": {
                      "type": "integer",
                      "format": "int64",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "402": {
            "description": "Platform Standard required"
          },
          "403": {
            "description": "Ask AI unavailable to this user or read scope missing"
          }
        }
      },
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Save an Ask AI answer for later",
        "description": "Requires `write:chat`, Ask AI app access and Platform Standard or higher.\nSaves the visible assistant answer in this account's own private general\nconversation. Shared, room, editor, hidden and cleared answers are refused.\nRepeating the request returns the existing bookmark without changing the\nanswer or the original save time. The normal `_meta` and\n`unread_notification_count` fields are additive.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "conversation_id",
                  "turn_id"
                ],
                "properties": {
                  "conversation_id": {
                    "type": "string",
                    "description": "Private conversation session id."
                  },
                  "turn_id": {
                    "type": "string",
                    "description": "Durable answer turn id, not the message row id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answer saved, or already saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "saved_answer"
                  ],
                  "properties": {
                    "saved_answer": {
                      "$ref": "#/components/schemas/AskAiSavedAnswer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "402": {
            "description": "Platform Standard required"
          },
          "403": {
            "description": "Ask AI unavailable to this user or write scope missing"
          },
          "404": {
            "description": "Answer not found or not visible to this account"
          }
        }
      }
    },
    "/ask_ai/saved_answers/{id}": {
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Remove a saved Ask AI answer",
        "description": "Requires `write:chat`, Ask AI app access and Platform Standard or higher.\nRemoves only this account's Ask AI bookmark in the current business, even\nafter its answer was cleared or removed. The answer is unchanged. A bookmark\nwith a personal note goes to Trash; a bare bookmark is removed permanently.\nIdempotent: an absent id or another account's id also returns 204.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Saved-answer bookmark id, not a conversation or message id."
          }
        ],
        "responses": {
          "204": {
            "description": "Bookmark no longer saved"
          },
          "401": {
            "description": "Authentication required"
          },
          "402": {
            "description": "Platform Standard required"
          },
          "403": {
            "description": "Ask AI unavailable to this user or write scope missing"
          }
        }
      }
    },
    "/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?"
                  },
                  "attachment_signed_ids": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "string"
                    },
                    "description": "Own uploaded files only; at most one video, which must be ready. A pending, failed, cancelled, or expired video returns 409 attachment_not_ready."
                  },
                  "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)"
                  },
                  "app_slug": {
                    "type": "string",
                    "description": "The app the person is in when they ask (a marketplace app slug —\na built-in app, an App Builder app or a mini app). The turn is\nbound to that app's agent, as the web panel does, so \"this\"\nand \"here\" mean that app. Honoured only when the app is enabled,\nopen to this person and has an agent they may use; otherwise it\nis ignored and the question is routed as usual. The response's\n`context_app` says which app was honoured.\n",
                    "example": "tasks"
                  },
                  "defer_follow_ups": {
                    "type": "boolean",
                    "default": false,
                    "description": "Desktop capability: with surface desktop_app, deliver the answer before\noptional suggestions. Suggestions arrive in a separate answer_extras\ncable frame with conversation_id, turn_id and follow_ups, and remain\navailable in message history. Other clients retain complete-frame metadata.\n"
                  },
                  "surface": {
                    "type": "string",
                    "description": "Which client sent the turn, for per-surface reporting: `mobile_app`\n(the phone app), `desktop_app` (the desktop app) or\n`chrome_extension`. An unknown or missing value is recorded as\n`mobile_app`, except that a request whose User-Agent carries an\n`Electron/` token is recorded as `desktop_app`.\n",
                    "enum": [
                      "mobile_app",
                      "desktop_app",
                      "chrome_extension"
                    ],
                    "default": "mobile_app"
                  }
                }
              }
            }
          }
        },
        "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": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "The caller's threads (the desktop sidebar's list)",
        "description": "Private `general` threads of the caller in this business, pinned first then\nnewest activity first (up to 25). `title` is the saved conversation name\nor the first question asked, routing token stripped; it is `null` for\nan unnamed empty thread. `pinned` is the account preference and `active`\nmarks the thread the next message lands in. Requires `read:chat`.\n`can_update_conversations` is true only when the caller may rename and\npin threads (`write:chat`); clients should hide those controls otherwise.\n\nWith `q`, only threads whose saved name contains the term or whose visible\nmessages match it (full-text or case-insensitive substring — the same\nmatching the per-thread `GET /ask_ai/conversations/{id}/messages?q=`\nuses). The same ordering and cap apply after filtering, so older\nmatching or pinned threads remain discoverable.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "History search term, trimmed, at most 100 characters. Blank lists every thread.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The threads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "can_update_conversations": {
                      "type": "boolean",
                      "description": "Whether the caller may rename and pin private threads."
                    },
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Thread"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "New chat",
        "description": "Closes the active thread and opens an empty one, which is returned. An\nactive thread that is still empty is reused. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The thread the next message lands in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreadResponse"
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/conversations/{id}/resume": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Continue an older thread",
        "description": "The thread becomes the active one — the next message lands in it and\nits transcript is the context — and the thread that was active is\narchived (an empty one too, so exactly one thread is ever active). The\ndesktop sidebar's one-list model (2026-09-15): any thread is\nresumable, none opens read-only. Resuming the already-active thread is\na no-op that returns it. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The thread id from the list (`conversations[].id`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The resumed thread, now active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreadResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not one of the caller's threads",
            "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}": {
      "patch": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Rename or pin a private conversation",
        "description": "Saves the caller's conversation name and/or pinned preference on the\nserver, so every client reading the thread list sees the same values.\nRequires `write:chat`. Only the caller's private `general` threads in\nthis business can be changed; shared and room conversations return 404.\nAt least one of `title` or `pinned` is required. Omitted values stay\nunchanged. Names are trimmed and repeated whitespace is collapsed;\nthe normalized name must contain 1–100 characters. `pinned` must be a\nJSON boolean. This operation does not change the active conversation,\ntranscript, or `updated_at` activity timestamp.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The thread session id from the conversation list.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "pinned": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated conversation, with standard API metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreadResponse"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "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": "Ask AI is unavailable to the caller or the token lacks write scope.",
            "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 one of the caller's private general conversations.",
            "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 supported preference, an invalid name, or a non-boolean pinned value.",
            "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"
                  ]
                }
              }
            }
          }
        }
      },
      "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 — on the\nperson's current thread only. Clearing an earlier thread clears its messages\nalone, leaves it closed, and answers `cleared.facts` and `cleared.cache` false.\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 and this is the current thread, also clears learned facts and cache (ignored for an earlier thread)",
            "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/suggestions": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Welcome-screen suggestion chips for the page the client is on",
        "description": "Page-aware starter questions for the chat's empty state, each kept only\nwhile the app it needs is accessible to this user, plus the agent roster\nthe same screen lists under them. Requires `read:chat`. An unknown or\nabsent `page_type` returns the default chips. The body is the same as the\n8.x Chrome extension door's `GET /api/v1/extension/ask_ai/suggestions`;\nboth answer from `AskAi::Suggestions`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "linkedin_profile",
                "gmail",
                "google_calendar",
                "competitor",
                "vendor",
                "mangoapps"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The chips and the agent roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "text": {
                            "type": "string",
                            "example": "Create a task from this email"
                          },
                          "icon": {
                            "type": "string",
                            "example": "check-square"
                          }
                        }
                      }
                    },
                    "agents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "icon": {
                            "type": "string"
                          },
                          "examples": {
                            "type": "array",
                            "maxItems": 3,
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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 the assistant's name (`assistant_name`, `assistant_name_custom`) for the Ask AI\nscreen and buttons, and minimal feature toggles including:\n- `saved_answers_enabled`: Whether this client may list saved Ask AI answers\n- `save_answers_enabled`: Whether this token may save or remove an answer (`write:chat`)\n- `create_support_ticket_enabled`: Whether Ask AI may file Service Desk tickets for this user\n- `escalation_enabled`: Whether to offer \"Get human help\" on an answer whose metadata carries `escalation_offer` (POST /ask_ai/escalate)\n- `show_create_ticket_button`: Whether to show the standing \"Create Support Ticket\" button (the Service Desk admin's opt-in)\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 settled (an earlier End, the ActionCable\nchannel, or the abandoned-session sweep), this endpoint returns\n`success: true` with `billed: false` and zeroes — safe to call twice.\nAnother user's session, or an unknown id, answers 404.\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"
          }
        }
      }
    },
    "/ask_ai/agents": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Agents the caller may @mention",
        "description": "The composer's Agent picker: every agent that is on for this business\nand that this person may use, sorted by name. Hidden and external-portal\nagents are never listed. Start a message with `@<alias>` to send it to\nthat agent. Requires `read:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The agent roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "agents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AskAiMentionableAgent"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiReadRefused"
          },
          "500": {
            "description": "The roster could not be loaded (`internal_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"
                  ]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/pinned_agent": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "The agent the conversation is pinned to",
        "description": "The \"Continuing with: <Agent>\" chip. When an agent's answer asks the\nperson a question back, the next turn goes to that agent; the pin is\nreleased once an agent answers without asking, and lapses 15 minutes\nafter it was set. `pinned_agent` is null when nothing is pinned.\nRequires `read:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The pin, or null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "pinned_agent": {
                      "nullable": true,
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AskAiPinnedAgent"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiReadRefused"
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Clear the agent pin",
        "description": "The person dismissed the \"Continuing with\" chip: the next turn is routed\nas usual. Answers 200 when nothing was pinned too. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The pin is cleared",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiSuccess"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/prompts": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "The Prompt Library picker",
        "description": "The system gallery (featured first, then by name) followed by this\nbusiness's own prompts (most recently updated first). Each of the two\nlists is capped at `limit` (200); `truncated` is true when either has\nmore. A prompt aimed at an agent this person cannot use is left out.\nRequires `read:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The prompts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiPromptsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiReadRefused"
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/prompts/{id}/use": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Use a prompt",
        "description": "The picker's \"Use\" click. Counts the use and returns what the composer\nneeds: the text to put in the draft and the agent to send it to. Nothing\nis sent — sending stays the person's action. Uses of a system gallery\nprompt count at most 30 times per person per hour; past that the call\nstill succeeds, the use is just not counted. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The prompt id from `GET /ask_ai/prompts` (digits only).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The prompt to fill the draft with",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "prompt": {
                      "$ref": "#/components/schemas/AskAiUsedPrompt"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "404": {
            "description": "Not a prompt this person can see (`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"
                  ]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/memory": {
      "get": {
        "tags": [
          "Ask AI"
        ],
        "summary": "What Ask AI remembers about the caller",
        "description": "The caller's remembered facts in this business (the web \"Personal\nMemory\" page), 50 per page, most recently used first. Only facts not yet\nforgotten are listed. `memory_enabled` says whether personal memory is\nturned on for this business. Requires `read:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive match on the fact's text, trimmed, at most 200 characters.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number. A value outside 1…`total_pages` is moved to the nearest page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The facts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiMemoryResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiReadRefused"
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Forget everything remembered about the caller",
        "description": "Forgets every remembered fact about the caller in this business, and\nstops what was said so far from being learned again. Other businesses\nthe person belongs to are untouched. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Forgotten",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "forgotten": {
                      "type": "integer",
                      "description": "How many facts were forgotten.",
                      "example": 12
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/memory/{id}": {
      "delete": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Forget one remembered fact",
        "description": "Forgets one of the caller's facts in this business. A fact that was\nalready forgotten answers 200 as well. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The fact id (`facts[].id`, digits only).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Forgotten",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiSuccess"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "404": {
            "description": "Not one of the caller's facts in this business (`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"
                  ]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/playbook_input": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Fill in the form a paused playbook is waiting on",
        "description": "A playbook step that needs more information pauses and answers with a\n`form_collector` whose `submit_url` is `/ask_ai/playbook_input?input_id=…`.\nSend that `input_id` and the form's values here to continue the run.\nOnly the fields the paused step asked for are kept, and only the person\nand business the form was raised for may answer it. Requires `write:chat`.\n\n`success: true` when the run went on: `text` says what it did next, and\n`ui_response` carries the next card when it paused again. `success:\nfalse` with `terminal: true` means the form can never be sent again\n(expired, already answered, its run ended or the playbook was turned\noff) — retire its Submit; `text` says which, in the reader's language.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "input_id"
                ],
                "properties": {
                  "input_id": {
                    "type": "string",
                    "description": "The `input_id` from the form's `submit_url`."
                  },
                  "data": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "The form's values, keyed by field name."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The submit was handled (see `success` and `terminal`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "text": {
                      "type": "string"
                    },
                    "ui_response": {
                      "type": "object",
                      "nullable": true
                    },
                    "terminal": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no form with that `input_id` is waiting for this person.\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": "The submitted text was blocked by the content policy (`blocked: true`)."
          }
        }
      }
    },
    "/ask_ai/confirm_tool": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Approve or decline a pending action",
        "description": "The Confirm / Cancel buttons under an answer that proposed an action\n(an answer whose metadata carries `confirmation_id`). Each confirmation\nruns at most once and only for the person and business that raised it.\nOn approve the server checks again that actions are still turned on\nfor the organization and that the agent is still available to this\nperson; a decline always goes through. Requires `write:chat`.\n\nWhen the approved step stops at the next approval in a chain, the\nresponse carries `requires_confirmation: true` and `confirmation` for\nit — draw new buttons from those.\n\nEvery refusal answers `error.code` `confirmation_failed` with\n`error.details.terminal`: `true` means the card is closed (already\nanswered, expired, or not yours) — retire its buttons; `false` means the\nsame card can be tried again once the cause clears. When the refusal has\na more specific cause, `error.details.reason` names it:\n`confirmation_other_surface` (answer it where it was asked),\n`step_up_required`, `capability_not_granted`, `work_context_restricted`\nor `work_context_check_failed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirmation_id"
                ],
                "properties": {
                  "confirmation_id": {
                    "type": "string",
                    "description": "The `confirmation_id` from the answer's metadata."
                  },
                  "approved": {
                    "type": "boolean",
                    "default": false,
                    "description": "true to run the action. Missing or false declines it — nothing is changed."
                  },
                  "conversation_id": {
                    "type": "string",
                    "description": "Optional. Used only when the confirmation was stored without\nits conversation, and only if it is one of the caller's own.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The confirmation was handled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiConfirmToolResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "description": "`confirmation_failed`: the agent is no longer available to this\nperson, or the app's work-context access policy now closes it to\nthem. Or the request was refused before it reached the action:\n`app_disabled`, `extension_disabled` or `insufficient_permissions`\n(the token lacks `write:chat`).\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": "`confirmation_failed`: the confirmation expired, was already\nanswered, belongs to someone else, or is waiting for approval on\nanother surface. Always `terminal: true`.\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": "`confirmation_failed`: `confirmation_id` is missing, the stored\nconfirmation names no agent that can run it, or action execution is\nturned off for this organization.\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"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`confirmation_failed`: processing failed. The message says whether\nnothing was changed or the action may already have run.\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": "`confirmation_failed`: the server could not check that the action\nis still permitted, or could not check the app's work-context\naccess policy — try again (`terminal: false`). Or\n`entitlement_unavailable`: the plan check failed.\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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/escalate": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get human help with an answer",
        "description": "\"Get human help\" under an answer whose metadata carries\n`escalation_offer` and a `turn_id` (offer it only while\n`settings.escalation_enabled` is true). Files a Service Desk ticket\nwith the question, the answer and the turn; when this person has no\nService Desk, sends the question to the admins of the app that answered\ninstead. Calling it again for the same turn returns what the first call\nsent rather than filing again. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "turn_id"
                ],
                "properties": {
                  "turn_id": {
                    "type": "string",
                    "description": "The answer's `metadata.turn_id`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The question was handed to a person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiEscalateResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "422": {
            "description": "`escalation_failed`: nobody is set up to receive help requests for\nthis person, the turn is not one of the caller's (or `turn_id` is\nblank), the ticket could not be created, or the app's admins could\nnot be reached. `error.message` is the sentence to show.\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": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/answer_feedback": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Rate an answer (thumbs up or down)",
        "description": "Saves an editable rating and optional details on the caller's exact answer.\nSend the answer's turn_id. A supplied unknown turn never falls back to a\ndifferent answer. Legacy conversation_id-only requests are accepted only\nwhen exactly one answer is identifiable. Saved answers remain rateable\nafter diagnostic traces expire. Repeated submissions update the same\nreport; switching to up clears the optional details. Staff review is\nseparate and is never exposed in conversation history. Requires write:chat.\nThe first vote may also update verified knowledge cited by this answer,\nat most once per person per entry per 24 hours.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rating"
                ],
                "properties": {
                  "rating": {
                    "type": "string",
                    "enum": [
                      "up",
                      "down"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "incorrect",
                      "missing",
                      "off_topic",
                      "technical",
                      ""
                    ],
                    "description": "Optional reason; an empty string clears the reason."
                  },
                  "comment": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Optional details; an empty string clears the comment."
                  },
                  "turn_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "The answer's `metadata.turn_id`."
                  },
                  "conversation_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Legacy fallback, only when it identifies exactly one answer."
                  },
                  "verified_entry_ids": {
                    "type": "array",
                    "description": "The answer's `metadata.provenance.verified_entry_ids`. Only the\nfirst 20 positive integers are read; ids this exact answer\ndid not cite are ignored. Optional for new clients.\n",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rating was accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "recorded": {
                      "type": "boolean",
                      "description": "True when the rating was saved, including an idempotent retry."
                    },
                    "feedback": {
                      "type": "object",
                      "properties": {
                        "rating": {
                          "type": "string",
                          "enum": [
                            "up",
                            "down"
                          ]
                        },
                        "reason": {
                          "type": "string"
                        },
                        "comment": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "422": {
            "description": "invalid_request: invalid rating, reason or comment, or no answer\nbelonging to this caller could be identified unambiguously.\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": "`service_unavailable`: the rating could not be saved — try again.\nOr `entitlement_unavailable`: the plan check failed.\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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/correct_answer": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Correct an answer",
        "description": "\"Correct this answer\": saves the person's corrected answer as a\nverified-knowledge entry waiting for an admin's approval — nothing goes\nlive until it is approved. HTML is stripped; the question is cut to\n1,000 characters and each answer to 5,000. At most 20 corrections per\nperson in any 24 hours. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "question",
                  "correct_answer"
                ],
                "properties": {
                  "question": {
                    "type": "string",
                    "description": "The question that was asked."
                  },
                  "correct_answer": {
                    "type": "string",
                    "description": "The answer it should have given."
                  },
                  "original_answer": {
                    "type": "string",
                    "description": "The answer that was given, kept for the reviewer."
                  },
                  "conversation_id": {
                    "type": "string",
                    "description": "The conversation the answer is in, kept for the reviewer. Also\nfinds the answer's trace when `turn_id` is not sent.\n"
                  },
                  "turn_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "The answer's `metadata.turn_id`; marks that answer's trace as corrected."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The correction was sent for review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "description": "The sentence to show; an admin is told where to approve it.",
                      "example": "Thanks — your correction was sent to your admins for review."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "422": {
            "description": "`correction_failed`: the question or corrected answer is blank, or the entry could not be saved.",
            "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": "`correction_failed`: the daily correction limit (20 in 24 hours) is reached.",
            "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": {
            "$ref": "#/components/responses/AskAiEntitlementUnavailable"
          }
        }
      }
    },
    "/ask_ai/composer_assist": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Preview a clearer or corrected message draft",
        "description": "Privately transforms the caller's chat message draft and returns it to\nthe composer. Nothing is posted, stored or logged. `mode` picks the\ntransformation:\n\n- `clarify` — turn rough thoughts into clear wording, preserving uncertainty and intent\n- `proofread` — minimal spelling, grammar and punctuation corrections; draft only\n- `rewrite` — clearer, with grammar and spelling fixed, same meaning and tone\n- `professional` — a polished workplace tone\n- `friendly` — a warm, approachable tone\n- `shorten` — much more concise, same key facts\n\nThe draft keeps its language, @mentions, links, names, dates and\nnumbers. Needs the Chat setting `ask_ai_composer_assist_enabled`.\n30 calls per person per hour. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text",
                  "mode"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "The draft. Must not be blank."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "rewrite",
                      "clarify",
                      "proofread",
                      "professional",
                      "friendly",
                      "shorten"
                    ]
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "chat_body"
                    ],
                    "description": "Optional canonical chat text; preserves mention, link and code tokens."
                  },
                  "context": {
                    "type": "object",
                    "description": "Optional for clarify only; requires ask_ai_enabled. Loaded and authorized on the server.",
                    "required": [
                      "scope",
                      "room_id",
                      "target_message_id"
                    ],
                    "properties": {
                      "scope": {
                        "type": "string",
                        "enum": [
                          "room",
                          "thread"
                        ]
                      },
                      "room_id": {
                        "type": "integer"
                      },
                      "target_message_id": {
                        "type": "integer"
                      },
                      "thread_root_id": {
                        "type": "integer",
                        "description": "Required for thread scope; the canonical root."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The transformed draft",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "description": "The transformed draft. The response has no `success` key."
                    },
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "changed",
                        "unchanged",
                        "needs_detail"
                      ],
                      "description": "Present for clarify and proofread. The original draft is returned for unchanged or needs_detail."
                    },
                    "hint": {
                      "type": "string",
                      "description": "A short prompt to add missing intent when needs_detail."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Context is deleted, inaccessible or outside the room."
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "description": "`feature_disabled`: composer assist is off for this business. Or\n`app_disabled`, `extension_disabled`, or `insufficient_permissions`\n(the token lacks `write:chat`).\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` (blank `text`), `text_too_long` (over 4,000\ncharacters) or `invalid_mode`.\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": "`budget_exhausted`: the organization's monthly AI budget is used up\nuntil the 1st. Or `rate_limited`: the hourly limit of 30 is reached.\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": "`service_unavailable`: the AI service did not return a draft — the\ncall does not count toward the hourly limit. Or\n`entitlement_unavailable`: the plan check failed.\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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/ask_ai/cable_token": {
      "post": {
        "tags": [
          "Ask AI"
        ],
        "summary": "Get a short-lived WebSocket credential",
        "description": "Issues a short-lived ActionCable credential for a client with no\nbrowser cookie (the desktop app), so it never puts its own session or\nAPI token in the WebSocket URL. Connect with\n`<websocket.url>?cable_token=<cable_token>` and subscribe to\n`AiResponseChannel`; the credential opens no other channel. It lasts\n`expires_in` seconds (one hour) and can be reused within that time so\nreconnects work — ask for a new one after it expires. Each connection\nchecks again that the account is active and still belongs to this\nbusiness. 60 per person per hour. Requires `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAiCableTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/AskAiPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/AskAiWriteRefused"
          },
          "429": {
            "description": "`rate_limited`: 60 credentials an hour already issued to this person.",
            "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": "`service_unavailable`: the credential could not be stored — try\nagain. Or `entitlement_unavailable`: the plan check failed.\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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/moment_shares": {
      "get": {
        "tags": [
          "Shared Moments"
        ],
        "summary": "My moments shared outside the company",
        "description": "The caller's own pages shared outside the company and not stopped, newest\nfirst. Only the kinds whose app the token may read are listed\n(read:recognition for recognitions, awards, work anniversaries and a year\nin recognition; read:contests for contest wins; read:training for\ncertificates).\n\n`page_open` says whether the page and its card answer now: a share whose\nrecognition was made private, whose app was turned off, or whose company\nturned the Shared moments switch off (`meta.sharing_enabled`) reads\n\"no longer shared\" until that changes; its `public_url` and\n`card_image_url` are then null.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's open shares",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "moment_shares",
                    "meta"
                  ],
                  "properties": {
                    "moment_shares": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MomentShare"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "sharing_enabled": {
                          "type": "boolean",
                          "description": "The company's Shared moments switch. While it is off, nothing is public.",
                          "example": true
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 25
                        },
                        "total_count": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "The token holds none of read:recognition, read:contests or read:training (`insufficient_permissions`)"
          }
        }
      },
      "post": {
        "tags": [
          "Shared Moments"
        ],
        "summary": "Share one of my moments outside the company",
        "description": "Opens the moment's public page and answers its link, its card picture and\na LinkedIn share link — the web Share panel's Share to LinkedIn / Copy link.\nSharing again after a stop reopens the same link.\n\nOnly the person the moment is about may share it, while it is public and\nlive and the company's Shared moments switch is on; anyone else, a private\nor revoked moment, or the switch off answers 422 `not_shareable` (the\npayloads that draw a moment carry the same answer as `can_share_outside`).\nRefused under an impersonation token. The token needs the moment's app's\nwrite scope: write:recognition, write:contests or write:training.\n\nWhat leaves the company is the card: the person's name, what they earned\nand the company — never the message, the giver or points. A certificate's\nlink is its verification page.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject_type"
                ],
                "properties": {
                  "subject_type": {
                    "type": "string",
                    "description": "The kind of record. `anniversary` is an award (a work anniversary); the web Share link's names (`recognition_post`, `contest_award`, `training_certificate`, `year`) are accepted too.",
                    "enum": [
                      "recognition",
                      "award",
                      "anniversary",
                      "contest_win",
                      "certificate",
                      "year_in_recognition"
                    ],
                    "example": "award"
                  },
                  "subject_id": {
                    "type": "integer",
                    "description": "The record's id (a recognition post, award, contest award or training certificate). For `year_in_recognition`, the year when `year` is not sent.",
                    "example": 4812
                  },
                  "year": {
                    "type": "integer",
                    "description": "For `year_in_recognition` — the year (one of `GET /recognitions/wrapped`'s `meta.available_years`; another year is a 404). Omitted, the current year.",
                    "example": 2026
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "moment_share"
                  ],
                  "properties": {
                    "moment_share": {
                      "$ref": "#/components/schemas/MomentShare"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown `subject_type` (`invalid_parameter`)"
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "The token lacks the moment's app's write scope (`insufficient_permissions`)"
          },
          "404": {
            "description": "No such record in this company (or a year the company has no recap for)"
          },
          "422": {
            "description": "The caller may not share this moment now (`not_shareable`)"
          }
        }
      }
    },
    "/moment_shares/{id}": {
      "delete": {
        "tags": [
          "Shared Moments"
        ],
        "summary": "Stop sharing a moment",
        "description": "Stops the caller's own share: its public page reads \"no longer shared\"\n(404) and its card stops showing on that page. Always allowed for the\nperson's own share — even after the moment was revoked or the switch\nturned off. Another person's share is a 404. The token needs the moment's\napp's write scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "moment_share"
                  ],
                  "properties": {
                    "moment_share": {
                      "$ref": "#/components/schemas/MomentShare"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token"
          },
          "403": {
            "description": "The token lacks the moment's app's write scope (`insufficient_permissions`)"
          },
          "404": {
            "description": "Not the caller's share"
          }
        }
      }
    },
    "/shares": {
      "get": {
        "tags": [
          "Share"
        ],
        "summary": "Where a record can be sent, and where it already went",
        "description": "The places that send to one colleague (\"Message a colleague…\",\n\"Email a colleague…\", \"Text a colleague…\"), each with the `token` to pass\nas `place` to GET /shares/people and as `target` to POST /shares, and the\nrecord's live shares the caller may take back (at most 10).\n`channels_available` says whether any room or channel can be searched\n(GET /shares/channels).\n\n**Token scope:** `write:shares`. 404 `not_found` for a record the caller\ncannot open; 403 `app_not_available` when its app is not open to them.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "shareable_type",
            "in": "query",
            "required": true,
            "description": "The record's type.",
            "schema": {
              "type": "string",
              "enum": [
                "Wiki",
                "SitePage",
                "TrainingCourse",
                "TrainingLesson",
                "HelpRequest",
                "HelpRequestComment",
                "Crm::Account",
                "Crm::Activity",
                "WorkspaceMessage",
                "RecognitionPost",
                "Award",
                "Contests::Award",
                "MaForms::FormTemplate"
              ],
              "example": "Wiki"
            }
          },
          {
            "name": "shareable_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 42
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The colleague places and the shares so far.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "people_places": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "token": {
                            "type": "string",
                            "description": "Opaque; pass back as-is."
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "chat",
                              "email",
                              "text"
                            ]
                          },
                          "label": {
                            "type": "string",
                            "example": "Email a colleague…"
                          },
                          "note": {
                            "type": "string",
                            "nullable": true,
                            "description": "Text only: what a text costs, as the web drawer says before the send."
                          },
                          "unavailable_reason": {
                            "type": "string",
                            "nullable": true,
                            "description": "Text only: why no colleague can be texted this month (the budget or the credits are spent); GET /shares/people then offers nobody."
                          }
                        }
                      }
                    },
                    "channels_available": {
                      "type": "boolean"
                    },
                    "nowhere_hint": {
                      "type": "string",
                      "nullable": true,
                      "description": "When no place at all is switched on: who can switch one on, in the reader's words."
                    },
                    "can_suggest_note": {
                      "type": "boolean",
                      "description": "Offer \"Suggest a line\" beside the note box (POST /shares/suggest_note) — false when this deployment has no AI or the record is staff-only, where the web panel shows no button."
                    },
                    "shares": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 9
                          },
                          "status": {
                            "type": "string",
                            "description": "`sent`, or `removed` once taken back.",
                            "example": "sent"
                          },
                          "place": {
                            "type": "string",
                            "description": "Where it went — the room",
                            "channel or colleague's name.": null,
                            "example": "#store-managers"
                          },
                          "destination_kind": {
                            "type": "string",
                            "description": "The system it went to: chat, messaging (Teams/Slack), hub, email, text.",
                            "example": "chat"
                          },
                          "note": {
                            "type": "string",
                            "nullable": true,
                            "description": "The sender's note."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "can_remove": {
                            "type": "boolean",
                            "description": "The caller may take it back (DELETE /shares/{id}): its sender or an admin, with a `destructive:shares` token."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (no `write:shares`), `app_not_available`, or `forbidden` for a proud moment asked under View-as or an impersonation token (only the person posts their own moment).",
            "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 record, or the caller cannot open it.",
            "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": [
          "Share"
        ],
        "summary": "Share a record",
        "description": "Sends the record's title, the caller's note and its link to one place.\nOne press at a time per record and place: a second press while the first\nis still going answers 409 `in_progress`, and the same record to the same\nplace under the same title within ten minutes answers 409 `duplicate`\n(with the earlier `share` when the caller may remove it).\n\n`target` is a channel's `token` (GET /shares/channels), or a person\nplace's `token` (GET /shares) together with `recipient_id` (GET /shares/people).\n\n**Token scope:** `write:shares`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shareable_type",
                  "shareable_id",
                  "target"
                ],
                "properties": {
                  "shareable_type": {
                    "type": "string",
                    "example": "Wiki"
                  },
                  "shareable_id": {
                    "type": "integer",
                    "example": 42
                  },
                  "target": {
                    "type": "string",
                    "description": "A channel token or a person place token."
                  },
                  "recipient_id": {
                    "type": "integer",
                    "description": "The colleague",
                    "for a person place.": null
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Optional; travels with the link."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shared.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "share": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 9
                        },
                        "status": {
                          "type": "string",
                          "description": "`sent`, or `removed` once taken back.",
                          "example": "sent"
                        },
                        "place": {
                          "type": "string",
                          "description": "Where it went — the room",
                          "channel or colleague's name.": null,
                          "example": "#store-managers"
                        },
                        "destination_kind": {
                          "type": "string",
                          "description": "The system it went to: chat, messaging (Teams/Slack), hub, email, text.",
                          "example": "chat"
                        },
                        "note": {
                          "type": "string",
                          "nullable": true,
                          "description": "The sender's note."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "can_remove": {
                          "type": "boolean",
                          "description": "The caller may take it back (DELETE /shares/{id}): its sender or an admin, with a `destructive:shares` token."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_available`, or `forbidden` for a proud moment asked under View-as or an impersonation 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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such record, or the caller cannot open it.",
            "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": "`duplicate` (already sent there; `details.share` when the caller may remove it) or `in_progress`.",
            "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": "`destination_unavailable` (the place is gone, or this token's chat scopes do not allow it), `target_required` (pick a place, or the colleague for a person place), `note_too_long`, `invalid`, or the place turning the colleague or room away in its own words: `chat_refused`, `email_refused`, `text_refused`, `hub_refused`, `not_a_member`, `unknown_target`, `unsupported` — the sender's to fix; retrying does not help.",
            "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"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "`delivery_failed` or `delivery_unknown` — the place did not answer (an outage), or answered with an error; the message says what to do, and a later retry may go through.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shares/suggest_note": {
      "post": {
        "tags": [
          "Share"
        ],
        "summary": "Suggest a line for the note box",
        "description": "One suggested line for the note sent with a share — the web Share\npanel's \"Suggest\" — drafted from facts the recipients can already see\n(the record's kind, title and age, never its body). It sends nothing:\nthe person edits, keeps or deletes it, then POST /shares sends it as\n`note`. Nothing is drafted for a staff-only record. Offer it only when\nGET /shares answers `can_suggest_note: true`.\n\n**Token scope:** `write:shares`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shareable_type",
                  "shareable_id"
                ],
                "properties": {
                  "shareable_type": {
                    "type": "string",
                    "example": "TrainingLesson"
                  },
                  "shareable_id": {
                    "type": "integer",
                    "example": 42
                  },
                  "target_name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "The place's name, to address the line to (optional)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The suggested line.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "note": {
                      "type": "string",
                      "maxLength": 220
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (no `write:shares`), `app_not_available`, or `forbidden` for a proud moment asked under View-as or an impersonation 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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such record, or the caller cannot open it.",
            "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": "`staff_only` (nothing to draft from), `ai_blocked` (the organization's AI is paused or its budget spent — the message says which), or `no_suggestion` (none came back; write one).",
            "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_unavailable` — no AI on this deployment.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shares/channels": {
      "get": {
        "tags": [
          "Share"
        ],
        "summary": "Rooms and channels the record can be posted in",
        "description": "The caller's chat rooms and channels, the Teams and Slack channels their\ncompany connected with sharing on, and the Hub groups they belong to,\nwhose name holds `q` (their most recent ones for a blank `q`), up to ten\nper system — only the rooms this token's chat scopes let it post in.\n`shared_at` says when the record already went there.\n\n**Token scope:** `write:shares`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "shareable_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "Wiki"
            }
          },
          {
            "name": "shareable_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 42
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching rooms and channels.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "token": {
                            "type": "string",
                            "description": "Pass as `target` to POST /shares."
                          },
                          "name": {
                            "type": "string",
                            "example": "store-managers"
                          },
                          "place": {
                            "type": "string",
                            "example": "OneMango chat"
                          },
                          "shared_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_available`, or `forbidden` for a proud moment asked under View-as or an impersonation 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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such record, or the caller cannot open it.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shares/people": {
      "get": {
        "tags": [
          "Share"
        ],
        "summary": "Colleagues a person place can send the record to",
        "description": "Active colleagues the chosen place can reach (Email: an email address;\nText: a verified mobile it may text; chat: anyone but the caller and\nanyone an information barrier keeps them from messaging), who can open\nthe record, whose name holds `q`. 20 per page.\n\n**Token scope:** `write:shares`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "shareable_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "Wiki"
            }
          },
          {
            "name": "shareable_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 42
            }
          },
          {
            "name": "place",
            "in": "query",
            "required": true,
            "description": "A `people_places[].token` from GET /shares.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of colleagues.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "avatar_url": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "more": {
                      "type": "boolean"
                    },
                    "unavailable_reason": {
                      "type": "string",
                      "description": "Text only, and only when no colleague can be texted this month: why (people is then empty)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_available`, or `forbidden` for a proud moment asked under View-as or an impersonation 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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such record, or the caller cannot open it.",
            "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_place` — `place` is not a person place from GET /shares.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/shares/{id}": {
      "delete": {
        "tags": [
          "Share"
        ],
        "summary": "Take a share back",
        "description": "Removes the post from the room or channel where the place allows it, and\ntakes the share off the record. `remote_removed: false` means it was\nremoved here but may still be in the place — `note` says what to do there.\nA share already removed answers 200 with `already_removed: true`, and\nwith the `share` itself only for its sender or an admin.\n\n**Token scope:** `destructive:shares` (minted on sign-in; not in the\nemployee's own list of scopes for a token made by hand, because an\nadmin's Remove reaches other people's shares). The sender or an admin may remove it; anyone who\ncannot open the record gets 404, as for a share that does not exist.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "share": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 9
                        },
                        "status": {
                          "type": "string",
                          "description": "`sent`, or `removed` once taken back.",
                          "example": "sent"
                        },
                        "place": {
                          "type": "string",
                          "description": "Where it went — the room",
                          "channel or colleague's name.": null,
                          "example": "#store-managers"
                        },
                        "destination_kind": {
                          "type": "string",
                          "description": "The system it went to: chat, messaging (Teams/Slack), hub, email, text.",
                          "example": "chat"
                        },
                        "note": {
                          "type": "string",
                          "nullable": true,
                          "description": "The sender's note."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "can_remove": {
                          "type": "boolean",
                          "description": "The caller may take it back (DELETE /shares/{id}): its sender or an admin, with a `destructive:shares` token."
                        }
                      },
                      "description": "Absent when an already-removed share is asked about by someone who is neither its sender nor an admin."
                    },
                    "remote_removed": {
                      "type": "boolean"
                    },
                    "note": {
                      "type": "string",
                      "nullable": true
                    },
                    "already_removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions`, or `forbidden` (only its sender or an admin may remove it).",
            "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`.",
            "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": "`never_sent` — the share failed, so there is nothing to remove.",
            "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"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "`remove_failed` — the place did not answer; try again.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/my_team/outstanding": {
      "get": {
        "tags": [
          "My Team"
        ],
        "summary": "What your team still owes",
        "description": "The /my_team Outstanding tab as JSON: what everyone below the caller on\ntheir reporting line still owes — training, policy acknowledgments,\nMust-Reads, onboarding tasks and the other kinds the tenant tracks —\nrolled up per direct report, with the people who owe the most listed\nand their items.\n\nRooted at the caller over their whole reporting line in this business;\nnothing is tenant-wide. `remind` on each item says whether the caller\ncould send that item's reminder.\n\n**`by_direct_report` is one page.** A company leader can have thousands\nof direct reports, so the lines come a page at a time (25, at most 100),\nthe most urgent line first, then the line with the most items, then by\nname; `by_direct_report_meta` counts every line before the page is cut.\n\nWhere a works council hides names, reads and confirmations of the people\nit covers are counted, not named (`names_hidden`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "One kind (a value `kinds[].key` lists). An unknown kind is a 422.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "person",
            "in": "query",
            "required": false,
            "description": "One team member's whole list. Someone off the caller's team is a 404.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "people_limit",
            "in": "query",
            "required": false,
            "description": "How many people to list (default 25, at most 100).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "items_per_person",
            "in": "query",
            "required": false,
            "description": "How many items each listed person carries (default 5, at most 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "by_direct_report_page",
            "in": "query",
            "required": false,
            "description": "The page of `by_direct_report` lines (default 1).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "by_direct_report_per_page",
            "in": "query",
            "required": false,
            "description": "Lines per page (default 25, at most 100).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The team's outstanding items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "outstanding": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "description": "Open items across the team"
                        },
                        "urgent": {
                          "type": "integer"
                        },
                        "people_count": {
                          "type": "integer",
                          "description": "People who owe something"
                        },
                        "kinds": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "total": {
                                "type": "integer"
                              },
                              "urgent": {
                                "type": "integer"
                              },
                              "app_slug": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "by_direct_report": {
                          "type": "array",
                          "description": "One page of lines — a direct report (or, behind one who left, their reports) and everyone below them.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "own": {
                                "type": "integer",
                                "description": "What they owe themselves"
                              },
                              "below": {
                                "type": "integer",
                                "description": "What their own team owes"
                              },
                              "total": {
                                "type": "integer"
                              },
                              "urgent": {
                                "type": "integer"
                              },
                              "leads_team": {
                                "type": "boolean"
                              },
                              "people_with_items": {
                                "type": "integer"
                              },
                              "by_kind": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "integer"
                                }
                              },
                              "names_hidden": {
                                "type": "boolean",
                                "description": "Someone on this line has their reads and confirmations left out (works council)"
                              }
                            }
                          }
                        },
                        "by_direct_report_count": {
                          "type": "integer",
                          "description": "Every line, counted before the page is cut"
                        },
                        "by_direct_report_truncated": {
                          "type": "boolean",
                          "description": "More lines exist than this page lists"
                        },
                        "by_direct_report_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"
                            }
                          }
                        },
                        "people": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "total": {
                                "type": "integer"
                              },
                              "urgent": {
                                "type": "integer"
                              },
                              "direct_report_id": {
                                "type": "integer",
                                "nullable": true
                              },
                              "by_kind": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "integer"
                                }
                              },
                              "names_hidden": {
                                "type": "boolean"
                              },
                              "items": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "kind": {
                                      "type": "string"
                                    },
                                    "kind_label": {
                                      "type": "string"
                                    },
                                    "type": {
                                      "type": "string"
                                    },
                                    "title": {
                                      "type": "string"
                                    },
                                    "priority": {
                                      "type": "string"
                                    },
                                    "due_at": {
                                      "type": "string",
                                      "format": "date-time",
                                      "nullable": true
                                    },
                                    "overdue": {
                                      "type": "boolean"
                                    },
                                    "url": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "remind": {
                                      "type": "object",
                                      "properties": {
                                        "available": {
                                          "type": "boolean"
                                        },
                                        "label": {
                                          "type": "string"
                                        },
                                        "everyone": {
                                          "type": "boolean"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "people_truncated": {
                          "type": "boolean"
                        },
                        "names_hidden": {
                          "type": "object",
                          "description": "Present when a works council keeps some reads and confirmations from being named.",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "nullable": true
                            },
                            "group_count": {
                              "type": "integer",
                              "nullable": true
                            },
                            "fewer_than": {
                              "type": "integer",
                              "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                            },
                            "reason": {
                              "type": "string"
                            },
                            "kinds": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "kind": {
                          "type": "string",
                          "nullable": true
                        },
                        "person_id": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "No business context, or the token lacks read:people_directory"
          },
          "404": {
            "description": "The caller has nobody reporting to them here, or the person is not on their team"
          },
          "422": {
            "description": "Unknown kind"
          }
        }
      }
    },
    "/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**Direct-message rows.** \"New mail from …\" and DM-mention rows carry up\nto 160 characters of the message, so they are served only where the\nMessages app's own API would serve them: the app is enabled for the\nbusiness AND the token may read messages (`read:messages`). Any other\ntoken sees its notifications without those rows — here, in\n`unread_count`, the piggyback `unread_notification_count`, the home\nzones and every write (a hidden row is \"not found\") — rather than a 403\nfor the whole inbox.\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\nSubsections with something unread come first, each set ordered by its newest\nnotification (`ordering: \"unread_first_then_newest\"`).\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": "Published, acknowledgment-required broadcasts the caller was\nSENT and has NOT acknowledged yet ({id, title} only), newest\npublished first, capped at 25 (a preview, not the whole set).\nA broadcast the caller only authored is never listed.\nIndependent of pagination/?filter=. The same set\nmeta.segment_counts.acknowledge counts.\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),\nor a Communications app admin or contributor role — company-wide or\nlimited to places and departments (Knowledge Layer X1). A limited one's\nsend reaches only the people inside their places and departments.\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": {
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional — the Communications campaign to put the broadcast\nin (choices: `GET /comms_hub/campaigns`); null (form-encoded: an empty\nvalue) takes it out,\nand leaving it out keeps whatever campaign it is in. The\nbroadcast is saved (and sent) either way: a campaign the\ncaller may not use, or one that is finished, comes back as\na `warnings` sentence saying why it is not in it — also for\nsomeone with no campaign to use. Naming the campaign it is\nalready in (or null for one in none) changes nothing.\n"
                  },
                  "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 by reference: ActiveStorage blob\n`signed_id`s the client already holds. Prefer\n`media_files`. The generic\n`POST /rails/active_storage/direct_uploads` endpoint is\ndisabled (403) and is not a way to obtain one. Subject\nto the same media rules; any invalid or expired\nreference fails the whole call with 422.\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"
                    }
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The SAME body, form-encoded, plus the attachment file parts. A file can only be sent this way — `format: binary` inside a JSON-only requestBody is something no generated client (and no Swagger UI) can actually post. Every other field is sent with its bracketed name, e.g. `broadcast[description]`, `broadcast[is_critical]`, `notification_recipient_group_ids[]`.",
                "properties": {
                  "broadcast[media_files][]": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The attachment file parts, uploaded in THIS request. Works with every authentication method, Bearer tokens included. They land in the broadcast's `media_files` (Drive) — the same attachments the web composer and the show API expose. Validated server-side against the broadcast media rules (image/pdf/video, per-file size cap, max 10 files, counted together with `media_signed_ids`); any invalid file fails the whole call with 422 `invalid_attachments` and nothing is persisted."
                  },
                  "broadcast[media_signed_ids][]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Attachments by reference — see the JSON body's `media_signed_ids`."
                  },
                  "broadcast[description]": {
                    "type": "string"
                  },
                  "broadcast[title]": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "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_files / 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. Three things land here: 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\npicked), a count of any attachment that could not be\nstored (\"1 of 2 attachments could not be attached…\"), so a\nbroadcast that went out without its photo says so, and why\nit is not in the campaign `campaign_id` named.\n",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No permission to create broadcasts, or app not enabled"
          },
          "402": {
            "description": "The cost step (texts and calls are paid). Asked only of a client\nthat opts in with the `X-Cost-Confirmation: ask` header (or\n`cost_confirmation=ask`): when the send draws more platform credits\nthan the cost threshold, nothing is sent and the answer is\n`{ error: { code: \"confirm_required\", message, details: { cost_preview } } }`;\nrepeat the same request with `cost_confirmed=1` to go ahead. A client\nthat does not opt in is never asked. For every client, a send the\nwallet or this month's budget cannot cover is refused with\n`{ error: { code: \"insufficient_credits\", message, details: { cost_preview } } }`.\n"
          },
          "422": {
            "description": "Validation failed (`validation_failed`); no recipient target supplied\nor it resolved to nobody (`no_recipients`); approval is required but\nthe request could not be submitted (`approval_required`); or it was\nheld for content review (`held_for_moderation`, with\n`error.details.broadcast_id`). On those two the broadcast is kept as\na draft, and `error.details.warnings` says why it is not in the\ncampaign `campaign_id` named, when it is not.\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 MangoApps starters written as Broadcasts (the\n                          Templates page's Template library set, which web\n                          Compose offers too), so an author can start from a\n                          pre-built composition. A starter is not a saved\n                          row: its `id` is null and `ref` names it.\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",
                          "ref",
                          "name",
                          "body",
                          "system_template"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The saved template's id; null for a MangoApps starter."
                          },
                          "ref": {
                            "type": "string",
                            "description": "How Compose names the template (`?template_id=`) — the id\nfor a saved template, `starter:<slug>` for a MangoApps starter.\n"
                          },
                          "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",
                          "ref",
                          "name",
                          "body",
                          "system_template"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The saved template's id; null for a MangoApps starter."
                          },
                          "ref": {
                            "type": "string",
                            "description": "How Compose names the template (`?template_id=`) — the id\nfor a saved template, `starter:<slug>` for a MangoApps starter.\n"
                          },
                          "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\nMedia can be ADDED by an edit: send the call as `multipart/form-data`\nwith `broadcast[media_files][]` file parts (and/or\n`broadcast[media_signed_ids][]`), exactly as on create. They are counted\nwith the files the draft already holds against the 10-file limit; an\ninvalid file fails the edit with 422 `invalid_attachments` and nothing\nis changed.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "broadcast"
                ],
                "properties": {
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional — the Communications campaign to put the broadcast\nin (choices: `GET /comms_hub/campaigns`); null (form-encoded: an empty\nvalue) takes it out,\nand leaving it out keeps whatever campaign it is in. The\nbroadcast is saved (and sent) either way: a campaign the\ncaller may not use, or one that is finished, comes back as\na `warnings` sentence saying why it is not in it — also for\nsomeone with no campaign to use. Naming the campaign it is\nalready in (or null for one in none) changes nothing.\n"
                  },
                  "broadcast": {
                    "$ref": "#/components/schemas/BroadcastInput"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same edit, form-encoded, plus the attachment file parts — the only content type a file can actually be sent on. Every other field takes its bracketed name, e.g. `broadcast[allow_comments]`.",
                "properties": {
                  "broadcast[media_files][]": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "File parts to ADD to the draft. Counted with the files it already holds against the 10-file limit; an invalid file fails the edit with 422 `invalid_attachments` and nothing is changed."
                  },
                  "broadcast[media_signed_ids][]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Attachments by reference to ADD to the draft. Prefer the file parts."
                  }
                }
              }
            }
          }
        },
        "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), a note naming any create-only key (`extra_user_ids`, `channels`) that was sent and therefore ignored, a count of any attachment that could not be stored, and why the broadcast is not in the campaign `campaign_id` named.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Admin access required, or broadcast is not a draft"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Validation failed"
          }
        }
      },
      "delete": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete a draft or scheduled broadcast",
        "description": "The web \"Delete\": only a `draft` or `scheduled` broadcast can be\ndeleted — deleting a scheduled one cancels its send, and a repeating\none every later send. Published broadcasts are archived instead\n(`POST /broadcasts/{id}/archive`). Cannot be undone.\n\nAuthorization is the web Delete's: an admin or a Broadcast /\nCommunications app admin, the broadcasts `delete` permission, the\nbroadcast's author, or a Communications admin limited to places and\ndepartments for a broadcast inside that reach.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:broadcasts` scope.\n",
        "responses": {
          "200": {
            "description": "Broadcast deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "id"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller may not delete this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Broadcast is published or archived (`not_deletable`), or could not be deleted (`delete_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 or scheduled broadcast now",
        "description": "Runs the model's `publish!` so the API matches the web path: recipient\nsnapshot, variant assignment, and the notification fan-out all run.\nA `scheduled` broadcast is sent now, as the web's \"Send now\" does.\n",
        "responses": {
          "200": {
            "description": "Broadcast published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "No permission to publish this broadcast, or it is already published or archived"
          },
          "402": {
            "description": "The cost step (texts and calls are paid). Asked only of a client\nthat opts in with the `X-Cost-Confirmation: ask` header (or\n`cost_confirmation=ask`): when the send draws more platform credits\nthan the cost threshold, nothing is sent and the answer is\n`{ error: { code: \"confirm_required\", message, details: { cost_preview } } }`;\nrepeat the same request with `cost_confirmed=1` to go ahead. A client\nthat does not opt in is never asked. For every client, a send the\nwallet or this month's budget cannot cover is refused with\n`{ error: { code: \"insufficient_credits\", message, details: { cost_preview } } }`.\n"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Could not publish broadcast"
          }
        }
      }
    },
    "/broadcasts/{id}/unschedule": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel a scheduled send (back to draft)",
        "description": "The web \"Cancel schedule\": a `scheduled` broadcast goes back to a\ndraft, its send time and repeat cleared. An approval request still\nopen on it is withdrawn with it — left open, approving it would send\nthe draft at once — and `approval_withdrawn` says whether one was.\nNothing changes unless both succeed.\n\nAuthorization is the web's: whoever may publish the broadcast.\n",
        "responses": {
          "200": {
            "description": "Moved back to draft",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "broadcast",
                    "approval_withdrawn",
                    "message"
                  ],
                  "properties": {
                    "broadcast": {
                      "$ref": "#/components/schemas/BroadcastDetail"
                    },
                    "approval_withdrawn": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller may not publish this broadcast"
          },
          "404": {
            "description": "Broadcast not found"
          },
          "422": {
            "description": "Broadcast is not scheduled (`not_scheduled`), or the schedule could not be canceled (`unschedule_failed`)"
          }
        }
      }
    },
    "/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\nit. Recipients only: a caller the broadcast was not SENT to — its author,\nor an admin or manager who can open it — is refused 403 `not_a_recipient`.\nThe broadcast payload's `can_acknowledge` says in advance whether this\ncall will succeed. Repeating it for a recorded acknowledgement answers 200.\n",
        "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"
          },
          "403": {
            "description": "The broadcast was not sent to the caller (`not_a_recipient`)"
          },
          "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\n**Works councils (Knowledge Layer X3).** Where a works council hides\nnames in read reports (General Settings › Countries), those viewers\nare left out of `viewers` before paging and counted in `names_hidden`;\n`unique_view_count` still counts everyone.\n\nWhile the audience's hidden group is smaller than the company's\nsmallest reportable group (Admin › Compliance › Retention & Legal\nHolds), the list never says how many of those few people viewed it:\n`names_hidden` is present whether or not it left anyone out, with a\nnull `count` and `group_count` and `fewer_than` (\"a few\"), and\n`unique_view_count` counts only the named viewers (equal to\n`total_count`) — so nothing subtracts down to a hidden person.\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\") — everyone, named or\nnot; only the named viewers while `names_hidden.count` is null (the\nhidden group is under the company's smallest group).\n"
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "The viewers this list names (`unique_view_count` minus `names_hidden.count`)."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    },
                    "names_hidden": {
                      "$ref": "#/components/schemas/BroadcastNamesHidden"
                    }
                  }
                }
              }
            }
          },
          "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+, or a Communications admin limited to places and departments\nwhose reach covers it). Anyone else gets `403`.\n\n**Works councils (Knowledge Layer X3).** Where a works council hides\nnames in confirmation reports, both tabs list only the people they may\nname (left out before paging); `names_hidden` counts the ones this tab\nleaves out. The counts in `meta` other than `total_count` count everyone.\n\nWhile the audience's hidden group is smaller than the company's\nsmallest reportable group, `acked_count` and `not_acked_count` count\nonly the named people, `total_recipients_count` stays the whole\naudience, and `names_hidden` (on both tabs, whether or not the tab\nleft anyone out) has a null `count` and `group_count` and\n`fewer_than` (\"a few\") — so `acked_count` minus this tab's `total_count` says\nnothing about a hidden person.\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 (only the named ones while `names_hidden.count` is null)."
                        },
                        "not_acked_count": {
                          "type": "integer",
                          "description": "Recipients who have not acknowledged (only the named ones while `names_hidden.count` is null)."
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Items matching the active type that this list names (its size)."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    },
                    "names_hidden": {
                      "$ref": "#/components/schemas/BroadcastNamesHidden"
                    }
                  }
                }
              }
            }
          },
          "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, parent_id,\nbody, mentioned_user_ids, created_at, author, attachments, replies });\nthe only difference is this returns the WHOLE feed page-by-page instead\nof just the latest 5. Pages over top-level comments, newest first\n(`created_at DESC`); each carries its visible replies, oldest first, in\n`replies` (threads are one level deep).\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 a comment; omit/null for a top-level\ncomment. Threads are one level deep, so this must be a\nTOP-LEVEL comment on the same broadcast (to answer a\nreply, send its `parent_id`) — anything else is `422`.\n"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "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 or admin)",
        "description": "Updates a comment's content. Permission matches the `can_edit` flag: a\ncompany admin or a Broadcast / Communications app admin, or the\ncomment's author for 15 minutes after posting while Communications\nallows editing posts and comments — the same rule as the web and phone\nedit. Anyone else gets `403`; an archived broadcast's comments are\nclosed (`403 comments_closed`). Sets `edited_at`. Re-runs content\nmoderation 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. An edit changes the words only — a\ncomment keeps the place in the thread it was posted in\n(any `parent_id` sent here is ignored). Each @mention is\nthe token `@[Name](mention:<user id>)`; a plain `@Name`\nfor a mention the comment already held keeps it.\n"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastCommentWriteResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is neither the comment's author nor an admin (`forbidden`)"
          },
          "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)",
        "description": "Soft-deletes a comment. Permission matches the `can_delete` flag and the\nweb delete visibility: the comment's author, a company admin, or a\nBroadcast / Communications app admin — the same rule as edit.\n\nDeleting a top-level comment deletes its replies with it (owner\ndecision #7810), in a background job seconds after the response; a\nreply deleted with its parent then answers 404 on this path like any\ndeleted comment.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:broadcasts` scope.\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"
          },
          "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 and is still an active\nmember (the people the acknowledgement reminders chase), 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",
                    "failed_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 recipients (active members who have not acknowledged) for whom at least one channel was sent or queued. A channel skipped for them (no email or verified phone on file, their preferences, the company's switch) or that failed does not count."
                    },
                    "failed_count": {
                      "type": "integer",
                      "description": "Number of pending recipients no channel reached — every channel was skipped or failed; the delivery table on the broadcast's stats page has each receipt and its reason."
                    },
                    "message": {
                      "type": "string"
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present while the audience's works-council group is under the company's smallest group. `resent_count` and `failed_count` then count only the named people; anyone counted, not named who still owes it was resent too, and is never counted here."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller cannot manage this broadcast"
          },
          "402": {
            "description": "The cost step (texts and calls are paid). Asked only of a client\nthat opts in with the `X-Cost-Confirmation: ask` header (or\n`cost_confirmation=ask`): when the send draws more platform credits\nthan the cost threshold, nothing is sent and the answer is\n`{ error: { code: \"confirm_required\", message, details: { cost_preview } } }`;\nrepeat the same request with `cost_confirmed=1` to go ahead. A client\nthat does not opt in is never asked. For every client, a send the\nwallet or this month's budget cannot cover is refused with\n`{ error: { code: \"insufficient_credits\", message, details: { cost_preview } } }`.\n"
          },
          "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 — or, for a recipient a works council keeps\nconfirmation reports from naming (Knowledge Layer X3), the\nstatus-hidden answer: the reminder goes only if they still owe the\nacknowledgment, and the answer is the same either way\n(`status_hidden: true`, no `reminder_count` / `last_reminder_at`,\nnever `already_acknowledged`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "title": "ReminderSent",
                      "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"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "title": "ReminderStatusHidden",
                      "required": [
                        "broadcast_id",
                        "user_id",
                        "status_hidden",
                        "message"
                      ],
                      "properties": {
                        "broadcast_id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "status_hidden": {
                          "type": "boolean",
                          "enum": [
                            true
                          ],
                          "description": "Whether this person had acknowledged is not shown (works council)."
                        },
                        "message": {
                          "type": "string",
                          "example": "If they haven't acknowledged yet, they've been reminded. Whether they have isn't shown here."
                        },
                        "names_hidden": {
                          "$ref": "#/components/schemas/BroadcastNamesHidden"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:broadcasts` scope.\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 company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed),\nor one the caller already holds from outside it (so it can be removed).\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`)"
          }
        }
      }
    },
    "/broadcasts/{id}/placements/{placement_id}/dismiss": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "placement_id",
          "in": "path",
          "required": true,
          "description": "The placement's `id` (a `placements[]` entry — see `CommsPlacement`).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Dismiss one of this broadcast's placements for the caller",
        "description": "The native twin of the web card's Dismiss on a placed broadcast (a\nshift's message, Top of Home, a store's pin). Records the same\ndismissal event the web card does, for the caller only; it never ends\nthe placement for anyone else (`end_now` does). The placement must\nbelong to this broadcast and be live, and the caller must be able to\nsee the broadcast. Needs `write:broadcasts`. A feed post's placement is\ndismissed through `POST /feeds/{id}/placements/{placement_id}/dismiss`.\n",
        "responses": {
          "200": {
            "description": "Dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dismissed": {
                      "type": "boolean",
                      "example": true
                    },
                    "placement_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Missing `write:broadcasts`, or the caller cannot see this broadcast"
          },
          "404": {
            "description": "Broadcast not found, or `{ error: { code: \"not_found\", message: \"Placement not found\" } }` — not this broadcast's, or not live"
          }
        }
      }
    },
    "/broadcasts/{id}/placements/{placement_id}/end_now": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Broadcast ID",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "placement_id",
          "in": "path",
          "required": true,
          "description": "The placement's `id`.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "End one of this broadcast's placements now (\"End now\")",
        "description": "Takes the placement down for everyone (Knowledge Layer E7). Allowed to\nthe author, or to someone the booking rule lets place it (a\nCommunications admin whose reach covers its audience; a store manager\nfor their store's pin). On a queued place (Top of Home) the booking\nwaiting right behind it starts now — `next_placement_id` names it.\nNeeds `write:broadcasts`. The same endpoint exists for a feed post:\n`POST /feeds/{id}/placements/{placement_id}/end_now`.\n",
        "responses": {
          "200": {
            "description": "Ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlacementEndResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Missing `write:broadcasts`, or `{ error }` — the caller may not end this placement (the refusal's own words)"
          },
          "404": {
            "description": "Broadcast not found, or `{ error: { code: \"not_found\", message: \"Placement not found\" } }` — not this broadcast's current placement"
          },
          "422": {
            "description": "`{ error }` — e.g. \"It has already ended.\""
          }
        }
      }
    },
    "/approvals/{id}/approve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Communications approval request id (a source's `pending_approvals` item's `approval_id`, or `id` from GET /comms_hub/approval_requests).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Approve a pending approval request",
        "description": "Records the caller's **approve** decision on a pending Communications\napproval request — for a post, a broadcast, an alert or a newsletter\nissue — 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`). Approving the last step releases what was\nheld: a post publishes (or goes back onto its schedule), a broadcast\nsends — unless its author has left the company (`message` says so).\n\n**Required Scopes:** `write:broadcasts`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "description": "Optional reviewer note, stored on the audit entry."
                  },
                  "seen": {
                    "type": "integer",
                    "description": "Optional. The version of the message the caller read —\n`content_version` as Unix seconds (`approval.content_version`). When\nthe item changed since, the decision is refused with 409\n`content_changed`, so nobody approves a version they didn't see.\nOmit it to decide without the check.\n"
                  }
                }
              }
            }
          }
        },
        "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`), the request was sent to the caller and their step was already decided (`no_longer_assigned`), Approvals is switched off (`approvals_disabled`), the token lacks the scope (`insufficient_permissions`), a Communications access policy limits the caller right now (`work_context_restricted`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No pending approval request with that id for this business (`not_found`)"
          },
          "409": {
            "description": "The item changed since the `seen` version the caller read, or was edited after it was sent for review — the review then starts again on the new version (`content_changed`). Read it again, then decide"
          },
          "422": {
            "description": "Decision could not be recorded (`approval_decision_failed`)"
          }
        }
      }
    },
    "/approvals/{id}/reject": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Communications approval request id (a source's `pending_approvals` item's `approval_id`, or `id` from GET /comms_hub/approval_requests).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reject a pending approval request",
        "description": "Records the caller's **reject** decision on a pending Communications\napproval request — for a post, a broadcast, an alert or a newsletter\nissue — mirroring `Apps::CommsHub::ApprovalsController#reject`. Appends\nan entry to the audit trail and closes the request (status →\n`rejected`); a held post stays in its author's drafts and they are\ntold why.\n\n**Required Scopes:** delete-level (owner decision 2026-10-04, #6721):\non top of `write:broadcasts`, rejecting asks `destructive:safety_hub`\nfor an alert and `destructive:broadcasts` for anything else — a post,\na broadcast or a newsletter issue.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "notes"
                ],
                "properties": {
                  "notes": {
                    "type": "string",
                    "description": "The reason — required; the author sees it and needs to know what to change."
                  },
                  "seen": {
                    "type": "integer",
                    "description": "Optional. The version of the message the caller read —\n`content_version` as Unix seconds (`approval.content_version`). When\nthe item changed since, the decision is refused with 409\n`content_changed`, so nobody approves a version they didn't see.\nOmit it to decide without the check.\n"
                  }
                }
              }
            }
          }
        },
        "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`), the request was sent to the caller and their step was already decided (`no_longer_assigned`), Approvals is switched off (`approvals_disabled`), the token lacks the scope (`insufficient_permissions`), a Communications access policy limits the caller right now (`work_context_restricted`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No pending approval request with that id for this business (`not_found`)"
          },
          "409": {
            "description": "The item changed since the `seen` version the caller read (`content_changed`) — read it again, then decide"
          },
          "422": {
            "description": "No `notes` (`reason_required`), or the decision could not be recorded (`approval_decision_failed`)"
          }
        }
      }
    },
    "/approvals/{id}/withdraw": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Communications approval request id (a source's `pending_approvals` item's `approval_id`, or `id` from GET /comms_hub/approval_requests).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Withdraw a pending approval request",
        "description": "The person who sent a post, broadcast, alert or newsletter issue for\napproval takes it back while it waits, mirroring\n`Apps::CommsHub::ApprovalsController#withdraw`. The request closes as\n`withdrawn` (with an audit entry), reviewers' open Approve/Reject\nbuttons retire, and the item is a draft again — a held post stays in\ndrafts, a scheduled broadcast returns to drafts rather than sending, a\nnewsletter issue leaves review. Only the requester or a company admin\nmay withdraw, and only while the request is pending; while Approvals\nis switched off, only the requester.\n\n**Required Scopes:** delete-level (owner decision 2026-10-04, #6721):\non top of `write:broadcasts`, withdrawing asks `destructive:safety_hub`\nfor an alert and `destructive:broadcasts` for anything else.\n",
        "responses": {
          "200": {
            "description": "Request withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApprovalDecision"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Caller is neither the person who sent it for approval nor a company admin (`forbidden`), Approvals is switched off and the caller did not send it (`approvals_disabled`), the token lacks the scope (`insufficient_permissions`), a Communications access policy limits the caller right now (`work_context_restricted`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No pending approval request with that id for this business (`not_found` / `no_pending_approval`)"
          },
          "422": {
            "description": "The request could not be withdrawn (`withdraw_failed`)"
          }
        }
      }
    },
    "/approvals/{id}/remind": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Communications approval request id (a My Posts row's `approval_request_id`, or `id` from GET /comms_hub/approval_requests).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remind the approvers of a pending request",
        "description": "The person who sent a post, broadcast, alert or newsletter issue for\napproval reminds everyone who can decide its current step, mirroring\nthe web request page's \"Remind approvers\"\n(`Apps::CommsHub::ApprovalsController#nudge`, one rule:\n`CommsHub::ApprovalNudge`). Only the requester, only while it is\npending, only while approval notifications are on, and at most once a\nday — the reminder shares the 24-hour claim the automatic reminders\nuse, and counts toward their cap. A My Posts row's `can_remind` says\nwhether this will be taken.\n\n**Required Scopes:** `write:broadcasts`.\n",
        "responses": {
          "202": {
            "description": "Reminder sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reached": {
                      "type": "integer",
                      "description": "How many reviewers it went to."
                    },
                    "message": {
                      "type": "string",
                      "example": "Reminder sent to 2 reviewers."
                    },
                    "next_remind_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the next reminder may go."
                    },
                    "approval": {
                      "$ref": "#/components/schemas/Approval"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "The caller did not ask for this approval (`forbidden`), Approvals is switched off (`approvals_disabled`), the token lacks the scope (`insufficient_permissions`), a Communications access policy limits the caller right now (`work_context_restricted`), or app not enabled (`app_disabled`)"
          },
          "404": {
            "description": "No approval request with that id for this business (`not_found`), or it is no longer pending (`no_pending_approval`)"
          },
          "409": {
            "description": "The communication was canceled (`comm_canceled`)"
          },
          "422": {
            "description": "Approval notifications are turned off for this business (`notifications_off`), nobody holds the step so the reminder reached no one (`no_approvers`), or it could not be sent (`remind_failed`)"
          },
          "429": {
            "description": "A reminder already went out in the last 24 hours (`already_reminded`); `error.details.next_remind_at` says when the next may go"
          }
        }
      }
    },
    "/audience_options": {
      "get": {
        "tags": [
          "Audience Options"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List audience-picker options (departments / locations / job titles / roles / groups / attribute dimensions)",
        "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`attribute_dimensions` is the catalog behind \"People who match all of\nseveral attributes\" — the `attribute_match` audience `POST /feeds`\naccepts (`{\"type\":\"attribute_match\",\"match\":{\"<key>\":[<id>, …]}}`). Each\nitem is one attribute section, `{ key, label, kind, values:[{ id, name,\nchip?, group? }], own_attribute? }`, narrowed to what the caller may\ntarget (a manager sees no admin-only attribute; a Communications admin\nor contributor limited to places and departments sees only the places\nand departments inside that reach, as `departments` and `locations`\ndo); it carries no\n`user_count`, and `q` filters the sections by label.\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`. A department counts\nthe people at or BENEATH it (Knowledge Layer X5 — \"Store departments\"\nincludes Bakery's people), active accounts only, no service accounts.\n\nA Communications admin or contributor limited to places and departments\n(Knowledge Layer X1) is offered only the `departments` and `locations`\ninside that reach; the send itself is capped the same way. The list is\nnarrowed when the client sends `reach=communications` (the broadcast\ncomposer), and always for a caller whose only way into this picker is\nthat grant — an admin, a manager or a holder of the broadcasts `create`\npermission keeps the full list for the alert composer.\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",
                "attribute_dimensions"
              ]
            }
          },
          {
            "name": "reach",
            "in": "query",
            "required": false,
            "description": "`communications` — list only what the caller may send a broadcast to\n(a Communications admin or contributor limited to places and\ndepartments sees their reach).\n",
            "schema": {
              "type": "string",
              "enum": [
                "communications"
              ]
            }
          },
          {
            "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",
                        "description": "A counted option; for `attribute_dimensions` an attribute section instead (`key`, `label`, `kind`, `values`).",
                        "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)."
                          },
                          "key": {
                            "type": "string",
                            "description": "attribute_dimensions only — the key to use in `match`."
                          },
                          "label": {
                            "type": "string",
                            "description": "attribute_dimensions only — the section's heading."
                          },
                          "kind": {
                            "type": "string",
                            "description": "attribute_dimensions only — enum / threshold / band / person."
                          },
                          "values": {
                            "type": "array",
                            "description": "attribute_dimensions only — the section's choices.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "oneOf": [
                                    {
                                      "type": "integer"
                                    },
                                    {
                                      "type": "string"
                                    }
                                  ]
                                },
                                "name": {
                                  "type": "string"
                                },
                                "chip": {
                                  "type": "string"
                                },
                                "group": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "own_attribute": {
                            "type": "boolean",
                            "description": "attribute_dimensions only — true when the section is one attribute of its own (one custom field, one location attribute) sharing its key with others; each is matched AND with the rest."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "type",
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "departments",
                            "locations",
                            "job_titles",
                            "roles",
                            "groups",
                            "attribute_dimensions"
                          ]
                        },
                        "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, broadcasts creator or Communications admin/contributor), or the app is not enabled"
          },
          "422": {
            "description": "Missing or unknown `type` (`invalid_type`)"
          }
        }
      }
    },
    "/links/resolve": {
      "get": {
        "tags": [
          "Links"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve a link to the phone page it opens, and whether the caller may open it",
        "description": "Answers from the same derivation the push payload and the inbox use\n(MobilePathConversion via DeepLinks::Allowlist), so a link opens the\npage its notification would.\n\n  * `in_app` — the link has a phone page; `mobile_path` is that `/m/`\n    page (open it in the shell's WebView).\n  * `allowed` — `false` when the caller is certainly refused: the link\n    belongs to another organization (`different_business`), its app is\n    off or not visible to them (`app_unavailable`), or the item's read\n    audience excludes them (`not_readable`). `true` otherwise — the page\n    still applies its own checks. `null` for an external link.\n  * `reason` — why `in_app` or `allowed` is false: `external`,\n    `not_in_app` (open it in the in-app browser), `different_business`,\n    `app_unavailable`, `not_readable`. `message` is the sentence to show.\n\nNever returns a title or any other field of the linked item; a missing\nitem and one the caller cannot read answer the same `not_readable`.\n\n**Required Scopes:** `read:organization` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore the link is read. Every sign-in token holds it, and so does a\ntoken a person makes by hand from this deploy on; a token made by hand\nbefore this deploy may not. A browser session is asked no scope.\n",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "An absolute http(s) URL, or a host-relative path (\"/apps/…\") resolved against the caller's organization. At most 2,048 characters.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The link, resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "link"
                  ],
                  "properties": {
                    "link": {
                      "type": "object",
                      "required": [
                        "url",
                        "in_app",
                        "mobile_path",
                        "allowed",
                        "reason",
                        "message"
                      ],
                      "properties": {
                        "url": {
                          "type": "string"
                        },
                        "in_app": {
                          "type": "boolean"
                        },
                        "mobile_path": {
                          "type": "string",
                          "nullable": true,
                          "example": "/m/apps/policy-hub/policies/42"
                        },
                        "allowed": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "external",
                            "not_in_app",
                            "different_business",
                            "app_unavailable",
                            "not_readable",
                            null
                          ]
                        },
                        "message": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "The token lacks `read:organization` (`insufficient_permissions`)."
          },
          "422": {
            "description": "Not a link (`invalid_url`) — blank, too long, or neither an http(s) URL nor a \"/\" path"
          }
        }
      }
    },
    "/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\nMy Day is **every** frontline user's own daily surface: there is no\nauthor-tier gate, so any authenticated caller — associate, store /\ndistrict / regional manager, or head-office admin — reaches it and gets\ntheir own per-user list (the same as the web, where the My Day tab shows\nfor every user). Access requires only the app being enabled and accessible\nto the caller and the My Day surface being on.\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\nLike `my_day`, this is every frontline user's own surface: there is no\nauthor-tier gate, so any authenticated caller — associate, store /\ndistrict / regional manager, or head-office admin — reaches it and gets\ntheir own per-user list. Access requires only the app being enabled and\naccessible to the caller and the My Day surface being on.\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",
                "onboarding",
                "signatures"
              ],
              "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",
                          "onboarding",
                          "signatures"
                        ]
                      },
                      "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"
                        }
                      ]
                    },
                    "also_from_this_shift": {
                      "type": "array",
                      "description": "Earlier handovers from the same shift nobody has read yet, newest first (two closers each left one); empty when there are none. Show them with `passdown`: acknowledging `passdown` with their ids in `also_ids` marks them read too, and each one whose `acknowledgeable` is true can be acknowledged on its own.",
                      "items": {
                        "$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."
          }
        }
      },
      "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. Refused for the\nhandover's author — they are not the next shift (offer the action only\nwhere the record's `acknowledgeable` is true). A second call on an already-acknowledged\nrecord answers 409 and leaves the original reader and timestamp intact.\nAcknowledging also marks read the earlier unread handovers from the same\nshift that the client names in `also_ids` — the `also_from_this_shift`\nrows it showed beside this one. Without `also_ids` only this handover\nis acknowledged: a row the reader was never shown is never marked read.\nAn id that is not an earlier unread handover from this one's shift, or\none the caller may not acknowledge, is skipped.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "also_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Ids of the `also_from_this_shift` rows shown with this handover.",
                    "example": [
                      41
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "passdown": {
                      "$ref": "#/components/schemas/ShiftPassdown"
                    },
                    "also_acknowledged": {
                      "type": "array",
                      "description": "The `also_ids` rows acknowledged with it; empty when none were.",
                      "items": {
                        "$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."
          },
          "409": {
            "description": "Already acknowledged (`already_acknowledged`)."
          },
          "422": {
            "description": "Refused — `own_handover` (the caller wrote it), `own_shift` (the caller worked a\nshift at this store that ended within 30 minutes of it — the handover is for\nthe next shift) or `invalid`.\n"
          }
        }
      }
    },
    "/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
                        },
                        "target_locations": {
                          "type": "array",
                          "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "name": {
                                "type": "string",
                                "example": "Store 042 - Downtown"
                              }
                            }
                          }
                        },
                        "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. `invalid_from_request` — the `from_request_id` is not a declined request of the caller's."
          }
        }
      }
    },
    "/locations": {
      "get": {
        "tags": [
          "Locations"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the caller's assigned, active work locations",
        "description": "The locations the authenticated user is assigned to in the current business,\nactive ones only, ordered by name — never the business's hidden Virtual\nLocation, which stands in for \"no location\" on a shift (#11123). `is_primary` marks the user's primary\nlocation (at most one). Geofence fields are present only when the location\nenforces one; `allowed_radius_meters` is null otherwise.\n\nThis is the caller's OWN list — an admin sees their own assignments here too,\nnot the tenant's directory.\n\n**Required Scopes:** `read:organization` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made by\nhand before this deploy may not. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "Assigned locations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "locations",
                    "meta"
                  ],
                  "properties": {
                    "locations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AssignedLocation"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer",
                          "description": "Number of locations in `locations`."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — the token lacks `read:organization` (`insufficient_permissions`), or the token's business context is missing."
          }
        }
      }
    },
    "/locations/{id}/departments": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "List a location's active departments",
        "description": "The active departments of one location in the caller's business,\nordered by name — what a shift form offers once a location is picked.\nInactive departments are left out, so every row's `active` is `true`.\n\nThe location may be any location of the business — not only the\ncaller's own assigned locations — and inactive locations are not\nrefused. A location id from another business, or one that does not\nexist, answers 404. The list is not paginated.\n\n**Required Scopes:** `read:organization`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made\nby hand before it may not. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The location's id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The location's active departments, A to Z",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "departments"
                  ],
                  "properties": {
                    "departments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LocationDepartmentsDepartment"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:organization` (\"Required scope: read:organization\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no location with this id in the caller's business\n(\"Location not found\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` — the department list could not be built\n(\"Could not fetch departments\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recipient_group_parameters/locations": {
      "get": {
        "tags": [
          "Recipient Group Parameters"
        ],
        "summary": "List the locations a recipient-group picker may offer",
        "description": "Every active, physical location of the caller's business, A to Z, as\n`{ id, name }` — the choices for a group whose `needed_for` answer\nincludes a `location` parameter. The system Virtual Location (where\nlocation-less team shifts are stored) is never offered. The answer is\na bare JSON array, not paginated.\n\n**Required Scopes:** `read:organization`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made\nby hand before it may not. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The business's active, physical locations, A to Z",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RecipientGroupParametersOption"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:organization` (\"Required scope: read:organization\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` — the list could not be built (\"Could not fetch locations\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recipient_group_parameters/departments": {
      "get": {
        "tags": [
          "Recipient Group Parameters"
        ],
        "summary": "List the departments a recipient-group picker may offer",
        "description": "Every organizational department of the caller's business, A to Z, as\n`{ id, name }` — the choices for a group whose `needed_for` answer\nincludes a `department` parameter. These are the company's\norganizational departments (not a location's floors and units, which\nare GET /locations/{id}/departments). No active filter is applied, so\nan inactive department is listed too. The answer is a bare JSON array,\nnot paginated.\n\n**Required Scopes:** `read:organization`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made\nby hand before it may not. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The business's organizational departments, A to Z",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RecipientGroupParametersOption"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:organization` (\"Required scope: read:organization\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` — the list could not be built (\"Could not fetch departments\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recipient_group_parameters/employees": {
      "get": {
        "tags": [
          "Recipient Group Parameters"
        ],
        "summary": "Search the people a recipient-group picker may offer",
        "description": "Members of the caller's business with an active membership, for a\ngroup whose `needed_for` answer includes an `affected_criteria`\nparameter (who is affected). Service accounts (Ask AI, System) are\nnever listed. Ordered by first name, then last name, and capped at 50\nrows — a typeahead, not a directory: pass `q` to narrow it. Without\n`q` the first 50 alphabetically are returned. The answer is a bare\nJSON array, not paginated.\n\n**Who may search:** someone who can send communications, as in the\nweb composer — the Communications app must be open to the caller,\nand the caller must be an admin or manager of the business, hold the\nbroadcasts `create` permission, or be a Communications communicator\n(the must-read permission, or a Communications app admin or\ncontributor, company-wide or for some places and departments).\nAnyone else gets `403 forbidden`; a business without Communications\ngets `403 app_disabled`, and a caller the app is not opened to\n`403 app_unavailable`.\n\n**Required Scopes:** `read:people_directory` (the people directory's\nown scope; admin-grade `read:users` satisfies it too). A token without\nit gets `403 insufficient_permissions` (\"Required scope:\nread:people_directory\") before anything else is checked. A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring matched against first name, last name, email, \"first last\" and \"preferred-name last\". Surrounding spaces are trimmed; blank means no filter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 50 matching members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "maxItems": 50,
                  "items": {
                    "$ref": "#/components/schemas/RecipientGroupParametersEmployee"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:people_directory` (\"Required scope: read:people_directory\").\n- `app_disabled` — \"The Broadcasts app is not enabled for this business.\": the business has not turned Communications on.\n- `app_unavailable` — \"The Broadcasts app is not available for your account.\": Communications is on, but not opened to the caller.\n- `work_context_restricted` — the business's work-context policy for Communications blocks the caller right now (off shift, off site or quiet hours); `error` is `{ code: work_context_restricted, message }`, with `code` repeated at the top level and `error_detail` repeating `{ code, message }`.\n- `forbidden` — \"Only someone who can send communications can choose people for a group.\": the caller is not a composer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` — the list could not be built (\"Could not fetch employees\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recipient_group_parameters/needed_for/{id}": {
      "get": {
        "tags": [
          "Recipient Group Parameters"
        ],
        "summary": "Which parameters a recipient group needs before sending",
        "description": "The parameters the sender must supply before the recipient group can\nresolve to people. An empty `parameters` array means the group\nresolves on its own and needs nothing.\n\nThe group may be a system-wide group or one owned by the caller's\nbusiness. An id that does not exist, or that belongs to another\nbusiness, is NOT a 404: it answers `200 { \"parameters\": [] }`.\n\nWhich group needs what, by its recipient type and key:\n- a `location` group whose key names `location_staff` /\n  `location_managers` needs `location` (fill from\n  /recipient_group_parameters/locations);\n- a `role` group whose key names `department_heads` needs\n  `department` (fill from /recipient_group_parameters/departments);\n- an `affected` group whose key names `affected_employees` needs\n  `affected_criteria` (fill from /recipient_group_parameters/employees).\n\nThe first two are skipped when the group's own criteria already pin\nits members (fixed users, roles, locations, departments, statuses,\nemployee type, tenure, schedules or title); the `affected_employees`\ngroup always asks. Every other group needs nothing.\n\n**Who may ask:** someone who can send communications, the same rule as\nGET /recipient_group_parameters/employees — the Communications app\nopen to the caller, and an admin or manager, the broadcasts `create`\npermission or a Communications communicator. Anyone else gets\n`403 forbidden`; a business without Communications gets\n`403 app_disabled`, and a caller the app is not opened to\n`403 app_unavailable`.\n\n**Required Scopes:** `read:organization`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:organization\")\nbefore anything else is checked. Every sign-in token holds it, and so\ndoes a token a person makes by hand from this deploy on; a token made\nby hand before it may not. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The recipient group's id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The parameters the group needs (empty when none, or when the group is not found)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "parameters"
                  ],
                  "properties": {
                    "parameters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecipientGroupParametersRequiredParameter"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:organization` (\"Required scope: read:organization\").\n- `app_disabled` — \"The Broadcasts app is not enabled for this business.\": the business has not turned Communications on.\n- `app_unavailable` — \"The Broadcasts app is not available for your account.\": Communications is on, but not opened to the caller.\n- `work_context_restricted` — the business's work-context policy for Communications blocks the caller right now (off shift, off site or quiet hours); `error` is `{ code: work_context_restricted, message }`, with `code` repeated at the top level and `error_detail` repeating `{ code, message }`.\n- `forbidden` — \"Only someone who can send communications can choose people for a group.\": the caller is not a composer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` — the answer could not be worked out (\"Could not determine parameters\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/practice-hub/scenarios": {
      "get": {
        "tags": [
          "Practice Hub"
        ],
        "summary": "List the published practice scenarios",
        "description": "The business's published practice scenarios — the Practice Library —\npaginated. Micro-drills and call-derived coach drills (scenarios\ntagged `drill`) are never listed, and neither are drafts, scenarios in\nreview or archived ones. Only scenarios owned by the caller's business\nare listed.\n\nOrdered trusted-first: `official`, then `curated`, then `community`\nvisibility; then higher average rating, more attempts, most recently\nupdated, and finally newest id — so paging is stable.\n\n**Required Scopes:** `read:practice_hub`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:practice_hub\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PracticeHubPage"
          },
          {
            "$ref": "#/components/parameters/PracticeHubPerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of published scenarios",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "scenarios",
                    "meta"
                  ],
                  "properties": {
                    "scenarios": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PracticeHubScenario"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PracticeHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:practice_hub` (\"Required scope: read:practice_hub\").\n- `app_disabled` — Practice Hub is not enabled for the business (\"Practice Hub is not enabled for this business.\").\n- `app_forbidden` — Practice Hub is not published to the caller (\"You don't have access to Practice Hub.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/practice-hub/attempts": {
      "get": {
        "tags": [
          "Practice Hub"
        ],
        "summary": "List the caller's own practice attempts",
        "description": "The caller's own practice attempts in their business, newest first\n(`created_at` descending, then id), paginated. Nobody else's attempts\nare listed here — a manager or admin sees only their own too.\n\n`score` is the scorecard's final score: a reviewer's overridden score\nwhen one was set, otherwise the AI's overall score; null until the\nattempt is scored.\n\n**Required Scopes:** `read:practice_hub`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:practice_hub\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PracticeHubPage"
          },
          {
            "$ref": "#/components/parameters/PracticeHubPerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the caller's attempts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attempts",
                    "meta"
                  ],
                  "properties": {
                    "attempts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PracticeHubAttempt"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PracticeHubPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:practice_hub` (\"Required scope: read:practice_hub\").\n- `app_disabled` — Practice Hub is not enabled for the business (\"Practice Hub is not enabled for this business.\").\n- `app_forbidden` — Practice Hub is not published to the caller (\"You don't have access to Practice Hub.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/muscle-memory/lessons": {
      "get": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "List or search the business's lessons",
        "description": "The business's lessons, never including sample lessons and never\nincluding forgotten ones (forgetting is permanent, so there is no list\nof them). Any member the app is published to can read them; the\nlessons describe operations, never people.\n\n**What is listed.** With no `status`, the active lessons — verified and\nunverified together. `status=unverified` is the review queue (active\nlessons nobody has confirmed yet), `status=verified` the confirmed\nactive ones, and `status=superseded` the lessons the system superseded\nbecause later evidence stopped supporting them. A superseded lesson\nstays visible but no longer informs Ask AI answers; these are the\ncandidates for `POST …/{id}/reinstate`. `status=recheck` (curators\nonly) is the app's \"Due for a re-check\" list: hand-written lessons\nconfirmed more than six months ago and not used since — confirm one\nagain (`POST …/{id}/verify`) to renew it; a caller who may not curate\ngets `403 forbidden`. `status` takes exactly those four values — there\nis no `all` (omit the parameter) and no `forgotten`.\n\n**Two shapes, decided by `q`.**\n- No `q` (or a blank one): a paginated list. The response carries\n  `meta` with `page`, `per_page`, `total` and `total_pages`. A `page`\n  past the end returns the last page, so read `meta.page` for the page\n  you got.\n- With `q`: a search, unpaginated and capped. The response carries\n  `count` (the rows returned, not the number of matches), `per_page`\n  (the cap that applied) and `truncated` (true when more lessons match\n  than were returned — `page` does not reach them; narrow the query\n  or filters instead). It carries no `meta`. `q` is cut to 200\n  characters. A search with no `status` uses the same recall Ask AI\n  reads — meaning plus keywords, active lessons at or above the\n  business's confidence floor only, in relevance order, so an\n  unverified suggestion (born below the floor) is never found that\n  way. Add `status` (for example `status=unverified&q=dock`) to\n  search the review queue: that keyword search covers the lessons\n  that status lists and honours `sort`.\n\n**Filters are refused, not ignored.** An unknown `status` is `400\ninvalid_status`; a `domain` that is neither a platform domain nor one\nthis business's lessons carry is `400 invalid_domain`. Both name the\naccepted values in `error.details.allowed`. A recognised domain with no\nlessons is an empty `200`. An unknown `sort` is not an error: it falls\nback to `relevance`.\n\n**Required Scopes:** `read:muscle_memory`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:muscle_memory\")\nbefore anything else is checked.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Narrow the list to one lifecycle state: `unverified` (active, not yet confirmed — the review queue), `verified` (active and confirmed), `superseded` (later evidence stopped supporting them; still listed, out of answers) or `recheck` (curators only — confirmed over six months ago and not used since; see `due_for_recheck`). Omit it for every active lesson. Exact lowercase match; anything else, including `all`, `forgotten` and `Verified`, is `400 invalid_status`.",
            "schema": {
              "type": "string",
              "enum": [
                "unverified",
                "verified",
                "superseded",
                "recheck"
              ]
            }
          },
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "description": "Only lessons in this operational domain. Exact lowercase match against the platform's domains (`safety_hub`, `scheduling`, `service_desk`, `leave_management`, `onboarding`, `rfp_desk`, `automation`, `operations`, `frontline_execution`, `procurement`, `timesheets`, `forms`, `asset_pro`, `mango_spend`, `employee_data`, `contracts`) plus any other domain this business's own lessons carry. An unrecognised value is `400 invalid_domain`; its `error.details.allowed` lists what this business accepts.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search text, at most 200 characters (longer is cut, not refused). Switches the response to the unpaginated search shape described above. Blank or whitespace-only means no search.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Order of the paginated list and of a status-filtered search: `relevance` (default — most recently offered to an answer first, then highest confidence), `confidence` (highest first) or `newest` (most recently learned first). An unrecognised value means `relevance`. A search with no `status` keeps the recall's own relevance order and ignores `sort`.",
            "schema": {
              "type": "string",
              "enum": [
                "relevance",
                "confidence",
                "newest"
              ],
              "default": "relevance"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, from 1, for the paginated list only (a search ignores it). A missing, non-numeric or non-positive value means page 1; a page past the end returns the last page.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Without `q`, rows per page: default 25, at most 100. With `q`, the cap on rows returned: default 10, at most 50. A missing, zero, negative or non-numeric value means the default; a larger value is lowered to the maximum.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of lessons (with `meta`), or — when `q` was given — the search result (with `count`, `per_page` and `truncated`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryLessonList"
                }
              }
            }
          },
          "400": {
            "description": "A filter value the endpoint does not recognise. `error.code` says which:\n- `invalid_status` — `status` is not `unverified`, `verified`, `superseded` or `recheck`. For `forgotten` the message adds that forgotten lessons are not listed anywhere.\n- `invalid_domain` — `domain` is neither a platform domain nor one this business's lessons carry.\n\n`error.details` is `{ \"field\": \"status\" | \"domain\", \"allowed\": [...] }`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_status",
                    "message": "Can't filter lessons by \"forgotten\". Use one of: unverified, verified, superseded, recheck. Forgotten lessons aren't listed on any surface — forgetting is permanent, so there is no list of them to return.",
                    "details": {
                      "field": "status",
                      "allowed": [
                        "unverified",
                        "verified",
                        "superseded",
                        "recheck"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryReadForbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Add a lesson (born verified)",
        "description": "Adds one lesson to the business's memory, as a curator. The lesson is\n**born verified** at confidence 0.8, above the confidence floor, so it\ncounts straight away — and informs Ask AI answers wherever the business\nhas recall switched on. Only a caller who holds the curate capability\nmay add one this way; a member who does not gets `403 forbidden`\n(whose message, when the business lets them suggest lessons, points at\nthe app, where they can suggest one for a curator to review — this API\nhas no suggest endpoint).\n\nSend one plain sentence as `lesson_text`. An AI step rewrites it into a\nsingle house-style claim, chooses the domain and a location when the\ntext names one, so the `claim` in the response is usually not your\nexact wording; this runs on the request and can take several seconds.\nIf the AI step is unavailable the text is stored as written, under the\n`operations` domain.\n\nA sentence that restates a lesson the business already holds is not\nstored twice: the existing lesson is reinforced and the caller is\nrecorded as having confirmed it, and the response is `200` with\n`outcome: reinforced` and that existing lesson. A new lesson is `201`\nwith `outcome: created`.\n\nText that looks like a credential or access code, and text content\nmoderation refuses, is rejected (`sensitive`, `content_moderation`).\n\n**Required Scopes:** `write:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "lesson_text"
                ],
                "properties": {
                  "lesson_text": {
                    "type": "string",
                    "minLength": 15,
                    "maxLength": 600,
                    "description": "The lesson, as one plain sentence — what the business knows to be true. Between 15 and 600 characters once surrounding whitespace is trimmed. A list or an object is refused (`invalid_parameter`), never turned into text."
                  }
                }
              },
              "example": {
                "lesson_text": "Dock 4 floods in heavy rain, so inbound trailers are rerouted to dock 2."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "A new lesson was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryCreateResult"
                }
              }
            }
          },
          "200": {
            "description": "The sentence matched a lesson the business already holds, so that lesson was reinforced and re-confirmed instead of duplicated (`outcome: reinforced`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryCreateResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryCurateForbidden"
          },
          "422": {
            "description": "The sentence was refused. `error.code` says why and `error.details.field` is `lesson_text`:\n- `invalid_parameter` — `lesson_text` was a list or an object, not a single text value.\n- `invalid_lesson` — the text could not be read as one plain sentence.\n- `invalid_length` — shorter than 15 or longer than 600 characters (a missing `lesson_text` lands here).\n- `sensitive` — it looks like a password, an account number or an access code; nothing was stored.\n- `content_moderation` — the business's content moderation refused it.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/MuscleMemoryInternalError"
          }
        }
      }
    },
    "/muscle-memory/stats": {
      "get": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Read the aggregate learning picture",
        "description": "The same figures as the app's Analytics tab: how many lessons the\nbusiness holds, how they split by domain and by the distiller that\nlearned them, how much is being recalled into answers, the measured\nlift on lessons that intervened, and how reliably the playbooks they\ngraduate into complete. Team totals only — nothing per person. Sample\nlessons are not counted.\n\nThis is its own capability, not the curate one: the caller must hold\nthe \"Who can see Muscle Memory analytics?\" capability (managers and\nadmins by default; the business can widen it to everyone). A member who\nlacks it gets `403 forbidden` here and can still read the lessons\nthemselves.\n\n`total`, `verified`, `unverified`, `avg_confidence` and `by_domain`\ncount **active** lessons only; `superseded` and `forgotten` are their\nown counts.\n\n**Required Scopes:** `read:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The aggregate statistics.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "stats"
                  ],
                  "properties": {
                    "stats": {
                      "$ref": "#/components/schemas/MuscleMemoryStats"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:muscle_memory` (\"Required scope: read:muscle_memory\").\n- `app_disabled` — Muscle Memory is not enabled for the business (\"Muscle Memory is not enabled for this business.\").\n- `forbidden` — Muscle Memory is not published to the caller (\"You don't have access to Muscle Memory.\"), or the caller lacks the analytics capability (\"You don't have access to the aggregate memory statistics for this business. You can still read the lessons themselves.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/muscle-memory/lessons/{id}": {
      "get": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Read one lesson with its influence history",
        "description": "One lesson, including `influence_history` — the most recent times it\nwas actually used in an answer (at most five): when, on which surface,\nfor which agent and domain. The list does not carry the history; this\nread does.\n\nReadable are active and superseded lessons. A forgotten lesson, a\nsample lesson, an id from another business and an id that does not\nexist all answer the same `404 not_found`.\n\n`influence_history` is empty for every lesson when the business has\nturned \"Keep a history of where each lesson was used\" off — the lesson's\n`recall_audit_enabled` field says which case you are looking at. The\n`user` key (who received the answer) appears only for a caller who is\na manager or above, and only when the business has also turned on\nrecording who received the answer (off by default); otherwise the key\nis absent.\n\n**Required Scopes:** `read:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MuscleMemoryLessonId"
          }
        ],
        "responses": {
          "200": {
            "description": "The lesson and its influence history.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "lesson"
                  ],
                  "properties": {
                    "lesson": {
                      "$ref": "#/components/schemas/MuscleMemoryLessonDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryReadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/MuscleMemoryLessonNotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Forget a lesson (permanent)",
        "description": "**Forgets the lesson, permanently.** Its status becomes `forgotten`:\nit disappears from every list and read on every surface, stops\ninforming Ask AI answers, and the learning jobs never learn it again.\nNothing can bring it back — `reinstate` refuses it (`not_superseded`),\n`verify` and `correct` refuse it (`forgotten`), and no screen or call\nrestores it. To have the lesson back, add it again with\n`POST /muscle-memory/lessons`. The row and its usage receipts are kept\nfor the record (this is not a hard delete), but no API read returns\nthem.\n\nThe `200` response is the last time the lesson is returned: its\n`status` is `forgotten`. When a person wrote the lesson (or their\nsuggestion was merged into it) they are told it was forgotten, and the\nopen \"review this lesson\" requests curators were holding for it are\nclosed. Repeating the call on a lesson that is already forgotten changes\nnothing and tells no one again: it answers `200` with the same status,\n`already_forgotten: true` and a `notice` saying so.\n\nNeeds the curate capability, and — because forgetting is a final act —\nthe `destructive:muscle_memory` scope, not `write:muscle_memory`.\n\n**Required Scopes:** `destructive:muscle_memory`. A\n`write:muscle_memory` token gets `403 insufficient_permissions`\n(\"Required scope: destructive:muscle_memory\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MuscleMemoryLessonId"
          }
        ],
        "responses": {
          "200": {
            "description": "The lesson, now forgotten (`status: forgotten`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryCurationResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryCurateForbidden"
          },
          "404": {
            "$ref": "#/components/responses/MuscleMemoryLessonNotFound"
          },
          "500": {
            "$ref": "#/components/responses/MuscleMemoryInternalError"
          }
        }
      }
    },
    "/muscle-memory/lessons/{id}/verify": {
      "post": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Confirm a lesson is true",
        "description": "Records that the caller reviewed the lesson and it holds: the caller\nbecomes `verified_by`, `verified_at` is set to now, and the lesson's\nconfidence rises by 0.1 (to at most 1.0). An unverified suggestion\nthat was below the confidence floor starts informing answers once it\nclears it. On this first confirmation, when a person wrote the lesson\n(or their suggestion was merged into it) they are told it was\nconfirmed — not while it is superseded. The open review requests\ncurators were holding for it are closed — except on a superseded\nlesson, whose question stays open.\n\n**Confirming again.** On a lesson that is `due_for_recheck` it renews\nthe check (`verified_by` and `verified_at` move to the caller and now)\nand leaves confidence where it is. On a lesson already confirmed and\nnot due for a re-check it changes nothing: the response is `200` with\n`already_verified: true` and a `notice` saying so, so a retry after a\ntimeout is safe.\n\n**A superseded lesson stays superseded.** Confirming one nobody has\nconfirmed yet records the review but does not bring it back: the\nresponse is `200`, the lesson's `status` is still `superseded`, it\nstays out of answers, and the body carries a `notice` saying so. One\nalready confirmed records this review too (`verified_by` and\n`verified_at` move to the caller; confidence is unchanged). Use\n`POST …/{id}/reinstate` to bring it back. A first confirmation of an\nactive lesson carries no `notice`.\n\nA forgotten lesson cannot be confirmed (`422 forgotten`).\n\nNeeds the curate capability.\n\n**Required Scopes:** `write:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MuscleMemoryLessonId"
          }
        ],
        "responses": {
          "200": {
            "description": "The lesson after the review. When it is superseded the response also carries a `notice`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryCurationResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryCurateForbidden"
          },
          "404": {
            "$ref": "#/components/responses/MuscleMemoryLessonNotFound"
          },
          "422": {
            "description": "- `forgotten` — the lesson was forgotten, so there is nothing to confirm; a forgotten lesson cannot be restored.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/MuscleMemoryInternalError"
          }
        }
      }
    },
    "/muscle-memory/lessons/{id}/correct": {
      "post": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Correct a lesson with a note",
        "description": "Attaches a correction `note` to the lesson and records the caller as\nhaving reviewed it. **A `note` is required.** The lesson's `claim` is\nnot rewritten: the note is kept beside it as `correction_note` and\ntravels with the lesson wherever it is shown or exported. The caller\nbecomes `verified_by`, `verified_at` is set to now, and confidence is\nlifted to at least the business's confidence floor (it is never\nlowered). A later correction replaces the earlier note. When a person\nwrote the lesson (or their suggestion was merged into it) they are told\nit was corrected, and the open review requests curators were holding\nfor it are closed.\n\n**A superseded lesson stays superseded.** Correcting it saves the note\nbut does not bring the lesson back: the response is `200`, `status` is\nstill `superseded`, and the body carries a `notice` saying so. Use\n`POST …/{id}/reinstate` to bring it back.\n\nThe note is held to the same rules as a lesson: trimmed, not blank,\nnot a credential or access code, at most 2,000 characters, and\ncontent moderation applies.\n\nNeeds the curate capability.\n\n**Required Scopes:** `write:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MuscleMemoryLessonId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note"
                ],
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "The correction, as plain text. Required — a missing or blank note is `422 note_required`. A list or an object is `422 invalid_parameter`; more than 2,000 characters is `422 invalid`."
                  }
                }
              },
              "example": {
                "note": "Dock 4 now drains within the hour; reroute only after a full day of rain."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The lesson with its correction note. When it is superseded the response also carries a `notice`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MuscleMemoryCurationResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryCurateForbidden"
          },
          "404": {
            "$ref": "#/components/responses/MuscleMemoryLessonNotFound"
          },
          "422": {
            "description": "The correction was refused; nothing changed. `error.code` says why:\n- `invalid_parameter` — `note` was a list or an object (`error.details.field` is `note`).\n- `note_required` — `note` was missing or blank.\n- `sensitive` — the note looks like a password, an account number or an access code.\n- `content_moderation` — the business's content moderation refused it.\n- `invalid` — the note is longer than 2,000 characters.\n- `forgotten` — the lesson was forgotten, so there is nothing to correct; a forgotten lesson cannot be restored.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/MuscleMemoryInternalError"
          }
        }
      }
    },
    "/muscle-memory/lessons/{id}/reinstate": {
      "post": {
        "tags": [
          "Muscle Memory"
        ],
        "summary": "Reinstate a superseded lesson (\"Still holds\")",
        "description": "A curator attests that a **superseded** lesson still holds. It goes\nback to `active`, its confidence is lifted back above the business's\nconfidence floor so it re-enters Ask AI recall, the caller becomes\n`verified_by`, and the marks that superseded it are cleared. The open\n\"this lesson may no longer hold\" requests curators were holding for it\nare closed.\n\n**Only a superseded lesson can be reinstated.** On an active lesson\nthe call is refused (`422 not_superseded`) rather than reported as a\nsuccess that changed nothing; on a forgotten lesson it is refused the\nsame way — reinstating cannot undo a forget. Find candidates with\n`GET /muscle-memory/lessons?status=superseded`.\n\nNeeds the curate capability.\n\n**Required Scopes:** `write:muscle_memory`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MuscleMemoryLessonId"
          }
        ],
        "responses": {
          "200": {
            "description": "The lesson, now active.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "lesson"
                  ],
                  "properties": {
                    "lesson": {
                      "$ref": "#/components/schemas/MuscleMemoryLesson"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/MuscleMemoryCurateForbidden"
          },
          "404": {
            "$ref": "#/components/responses/MuscleMemoryLessonNotFound"
          },
          "422": {
            "description": "- `not_superseded` — \"That lesson isn't superseded, so there's nothing to reinstate.\" The lesson is active or forgotten; nothing changed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/MuscleMemoryInternalError"
          }
        }
      }
    },
    "/asset_pro/assets/{id}": {
      "get": {
        "tags": [
          "Asset Pro"
        ],
        "summary": "Get one asset",
        "description": "One asset of the caller's business, with its category, holder,\nlocation and service-ticket counters.\n\nWho may read it:\n- the person the asset is assigned to, whatever their role;\n- an Asset Pro admin — a role in the tenant's Asset Pro admin roles,\n  an app admin of Asset Pro, or a person Asset Pro settings name as\n  one who may maintain vendors, contracts and categories;\n- a person in the tenant's Asset Pro manager roles, or named in Asset\n  Pro settings as one who may manage assets, for an asset held by\n  someone in their reporting chain (direct or indirect report).\n\nAnyone else gets 403 \"Access denied\", even with the app open to them.\n\n**Required Scopes:** `read:asset_pro`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:asset_pro\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The asset's id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The asset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "asset"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "asset": {
                      "$ref": "#/components/schemas/AssetProAssetDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Two body shapes.\n\nThe error envelope (`{ \"error\": { \"code\", \"message\" } }`):\n- `insufficient_permissions` — the token lacks `read:asset_pro` (\"Required scope: read:asset_pro\").\n\nA plain `{ \"success\": false, \"message\": \"…\" }` body:\n- \"AssetPro is not enabled for your organization\" — Asset Pro is not enabled for the business (or not configured on this install).\n- \"You don't have access to AssetPro\" — Asset Pro is not published to the caller.\n- \"Access denied\" — the caller is not the asset's holder, an Asset Pro admin, or a manager of its holder.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/AssetProFailure"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No asset with this id in the caller's business. Plain body:\n`{ \"success\": false, \"message\": \"Asset not found\" }`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetProFailure"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/booking_pages": {
      "get": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "List the caller's booking pages",
        "description": "The booking pages the caller owns or hosts on as a team member, newest\nfirst (`created_at` descending), paginated. An Appointment Scheduler\nadmin gets every booking page in the business; each row's\n`relationship` says how the caller reaches it.\n\n`bookable` folds in the business's **Allow booking via public /meet\npages** setting, so it is never `true` for a link that would answer\n\"unavailable\"; `public_booking_disabled` says when that setting is the\nreason.\n\n**Required Scopes:** `read:appointment_scheduler`. A token without it\ngets `403 insufficient_permissions` (\"Required scope:\nread:appointment_scheduler\") before anything else is checked.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Pages per result page. Clamped to 1..100; a blank value means 50. `limit` is accepted as an alias.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the caller's booking pages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking_pages",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking_pages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentSchedulerBookingPage"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/AppointmentSchedulerPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerForbidden"
          }
        }
      }
    },
    "/appointment_scheduler/appointments": {
      "get": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "List appointments the caller can read",
        "description": "The appointments the caller may read — ones they host, ones booked on\na page they own (round-robin, collective and group bookings on a team\npage often land on a teammate), and collective meetings they attend as\na recorded co-host. An Appointment Scheduler admin reads every\nappointment in the business. Each row's `can_reschedule` and\n`can_cancel` say whether the caller may move or cancel it.\n\n`filter`:\n- `upcoming` (the default, and what any other value means) —\n  confirmed appointments that have not ended yet, so a meeting in\n  progress is included. Earliest start first.\n- `past` — confirmed and no-show appointments that have ended. Latest\n  start first.\n- `cancelled` — cancelled appointments, and the originals a reschedule\n  replaced (status `rescheduled`). Most recently changed first.\n\n**Required Scopes:** `read:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "past",
                "cancelled"
              ],
              "default": "upcoming"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Appointments per page. Clamped to 1..100; a blank value means 25. `limit` is accepted as an alias.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of appointments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "filter",
                    "appointments",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "filter": {
                      "type": "string",
                      "enum": [
                        "upcoming",
                        "past",
                        "cancelled"
                      ],
                      "description": "The filter applied."
                    },
                    "appointments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/AppointmentSchedulerPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerForbidden"
          }
        }
      }
    },
    "/appointment_scheduler/appointments/{id}": {
      "get": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Get one appointment",
        "description": "One appointment the caller may read, with the invitee's intake\nanswers, their time zone, the cancellation reason, the ten most\nrecent history entries, and the bookings a move links it to.\n\n**Required Scopes:** `read:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          }
        ],
        "responses": {
          "200": {
            "description": "The appointment",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "appointment"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "appointment": {
                      "$ref": "#/components/schemas/AppointmentSchedulerAppointmentDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerForbidden"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/availability": {
      "get": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Preview a booking page's availability",
        "description": "What an invitee would be offered on one of the caller's booking pages\n(one they own or host on as a team member; admins: any page), for ONE\nmeeting type — a page's meeting types have different durations and so\noffer different times.\n\nWithout `date`, the dates in the meeting type's booking window that\nhave at least one offerable slot (`available_dates`). With `date`, the\nofferable slots that day (`date` and `slots`). `unbookable_reason` is\n`no_host` when none of the page's hosts is still an active member of\nthe business — the reason a scan comes back empty — and null otherwise.\n\n**Required Scopes:** `read:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "booking_page_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "appointment_type_id",
            "in": "query",
            "description": "An active meeting type on that page. Defaults to the page's first active meeting type.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "A day (YYYY-MM-DD) on the booking page's calendar.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page's availability for that meeting type",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking_page",
                    "meeting_type",
                    "timezone",
                    "unbookable_reason"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking_page": {
                      "type": "object",
                      "required": [
                        "id",
                        "title"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        }
                      }
                    },
                    "meeting_type": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "duration_minutes"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "duration_minutes": {
                          "type": "integer"
                        }
                      }
                    },
                    "timezone": {
                      "type": "string",
                      "description": "The booking page's IANA time zone."
                    },
                    "unbookable_reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "no_host",
                        null
                      ]
                    },
                    "date": {
                      "type": "string",
                      "format": "date",
                      "description": "Present when `date` was sent."
                    },
                    "slots": {
                      "type": "array",
                      "description": "Present when `date` was sent.",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentSchedulerSlot"
                      }
                    },
                    "available_dates": {
                      "type": "array",
                      "description": "Present when no `date` was sent.",
                      "items": {
                        "type": "string",
                        "format": "date"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "- \"booking_page_id is required\"\n- \"Invalid date — use YYYY-MM-DD\"\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerForbidden"
          },
          "404": {
            "description": "\"Booking page not found\" — no such page, or not one the caller owns or hosts on (admins: any page).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "\"No active meeting type found for this booking page\" — the page has none, or `appointment_type_id` is not an active meeting type on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/appointments/{id}/reschedule_slots": {
      "get": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Times an appointment can move to",
        "description": "The availability a move of this appointment can use: its own booking\npage and meeting type, with the appointment itself left out of the\nmeeting type's daily cap. Without `date`, the dates with at least one\nofferable slot (`available_dates`); with `date`, that day's slots.\nSend a returned `starts_at` to `POST .../reschedule`.\n\nOnly for an appointment the caller may change (its host, its booking\npage's owner, or an admin) that is confirmed and has not started.\n\n**Required Scopes:** `read:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          },
          {
            "name": "date",
            "in": "query",
            "description": "A day (YYYY-MM-DD) on the booking page's calendar.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Where the appointment can move",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "timezone",
                    "unbookable_reason"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "timezone": {
                      "type": "string",
                      "description": "The booking page's IANA time zone."
                    },
                    "unbookable_reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "no_host",
                        null
                      ],
                      "description": "`no_host` when none of the page's hosts is still an active member — why the result is empty."
                    },
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "slots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentSchedulerSlot"
                      }
                    },
                    "available_dates": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "date"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "\"Invalid date — use YYYY-MM-DD\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerCannotChange"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "\"This appointment can't be rescheduled\" — it is not confirmed, or it has already started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/appointments/{id}/cancel": {
      "post": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Cancel an appointment",
        "description": "Cancels a confirmed appointment that has not started, as its host.\nThe calendar event is removed, any reserved room is released, and the\ninvitee is normally emailed a cancellation (not for a sample-data\nbooking). The other hosts and participants are told as the business's\nnotification settings say.\n\nA repeat of a cancel that already landed — a retry after a timeout —\nanswers 200 with `already_applied: true`; nothing more is cancelled,\nsent or recorded, and its `reason` is not stored.\n\n**Required Scopes:** `destructive:appointment_scheduler`. A\n`write:appointment_scheduler` token is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Optional reason, shown to the invitee. Longer than 500 characters is refused (422 `invalid`), never clipped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled (or already cancelled)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "already_applied",
                    "appointment"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "already_applied": {
                      "type": "boolean"
                    },
                    "appointment": {
                      "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerCannotChange"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused by the booking service; `code` says why:\n- `not_cancellable` — it is no longer confirmed, or it has started.\n- `not_found` — it was deleted while the request ran.\n- `invalid` — the save failed, e.g. a `reason` over 500 characters.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/appointments/{id}/no_show": {
      "post": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Mark an appointment a no-show",
        "description": "Records that the invitee didn't come, as the appointment's host, its\nbooking page's owner or an Appointment Scheduler admin. Only a past,\nconfirmed appointment can be marked. Bookkeeping only: no email is\nsent and the calendar is not changed, and a follow-up email not yet\nsent (it goes about 12 hours after a meeting) is skipped.\n\nA repeat of a mark that already landed answers 200 with\n`already_applied: true`. `DELETE` on the same path clears the mark.\n\n**Required Scopes:** `write:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Marked a no-show (or already marked)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "already_applied",
                    "appointment"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "already_applied": {
                      "type": "boolean"
                    },
                    "appointment": {
                      "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerCannotChange"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "`code: not_markable` — it has not ended yet, or it is not confirmed (cancelled or moved).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Clear an appointment's no-show mark",
        "description": "Returns a no-show to confirmed, as its host, its booking page's owner\nor an Appointment Scheduler admin. If the meeting ended within the\nlast 3 days and its follow-up email was not sent, the follow-up can\nthen go out. Anything not marked a no-show — including a repeat of a\nclear that already landed — answers 422 `not_marked`.\n\n**Required Scopes:** `write:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "already_applied",
                    "appointment"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "already_applied": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "appointment": {
                      "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerCannotChange"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "`code: not_marked` — the appointment isn't marked a no-show.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/appointment_scheduler/appointments/{id}/reschedule": {
      "post": {
        "tags": [
          "Appointment Scheduler"
        ],
        "summary": "Move an appointment to a new time",
        "description": "Moves a confirmed appointment that has not started to a new start\ntime, as its host. The new time must be a slot the booking page\noffers for that meeting type (see `reschedule_slots`). A replacement\nappointment is created and returned; the original keeps status\n`rescheduled` and its id is `rescheduled_from_id`. On a round-robin or\ncollective page the replacement can go to a different host.\n\nThe invitee is normally emailed the new time (not for a sample-data\nbooking) — the business decides whether that is a dedicated \"moved\"\nemail or a plain confirmation — and the entry already on their\ncalendar is updated in place.\n\nA repeat of a move that already landed at this time answers 200 with\n`already_applied: true` and the replacement it made; nothing more is\nbooked or sent.\n\n**Required Scopes:** `write:appointment_scheduler`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppointmentSchedulerAppointmentId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "starts_at"
                ],
                "properties": {
                  "starts_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "The new start, ISO 8601 with its UTC offset, on a date that exists — e.g. `2026-09-15T13:00:00Z` or `2026-09-15T09:00:00-04:00`. A time without an offset is refused."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved (or already moved to this time)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "already_applied",
                    "rescheduled_from_id",
                    "appointment"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "already_applied": {
                      "type": "boolean"
                    },
                    "rescheduled_from_id": {
                      "type": "integer",
                      "description": "The appointment that was moved (the id in the path)."
                    },
                    "appointment": {
                      "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "- \"starts_at (ISO 8601) is required\"\n- \"starts_at must be an ISO 8601 time with its UTC offset, on a date that exists — e.g. 2026-09-15T13:00:00Z or 2026-09-15T09:00:00-04:00\"\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AppointmentSchedulerUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AppointmentSchedulerCannotChange"
          },
          "404": {
            "description": "\"Appointment not found\" — no such appointment, or not one the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused by the booking service; `code` says why:\n- `slot_taken` — the new time is not offerable (taken, outside availability, or past a limit); pick another slot and retry.\n- `not_cancellable` — it is no longer confirmed, or it has started.\n- `not_found` — it was deleted while the request ran.\n- `invalid` — the replacement failed to save.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/bookings": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "List bookings",
        "description": "The bookings the caller may read in their business, newest start first\n(`start_time` descending, then `id` descending), paginated. Every\nstatus is listed unless `status` narrows it.\n\nWhich bookings are listed is the tenant's \"Who can see everyone's\nbookings?\" policy (Bookings → Settings → User Permissions; default:\nmanagers): a caller it admits sees every booking in\nthe business; on the `managers` policy a person with direct reports\nalso sees their reports' bookings; everyone else sees only their own.\n\nFilters:\n- `status` — one status (`?status=confirmed`) or several\n  (`?status[]=confirmed&status[]=completed`). A hash-shaped value is\n  ignored. An unknown status matches nothing.\n- `start_date` — bookings starting on or after that calendar day, read\n  on each booking's room's clock (its own time zone, else the\n  workspace's).\n- `end_date` — bookings ending on or before the end of that calendar day,\n  on the same clock.\n  An unparseable date is ignored (no filter), never a 400.\n\nThe response also carries `X-Total-Count`, `X-Total-Pages`,\n`X-Current-Page` and `X-Per-Page` headers.\n\n**Required Scopes:** `read:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:bookings\")\nbefore anything else is checked. The app's sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Bookings per page. A numeric value is clamped to 1..100; a blank or non-numeric value means 25.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only bookings in this status. Repeat as `status[]=` for several.",
            "schema": {
              "type": "string",
              "enum": [
                "confirmed",
                "cancelled",
                "completed",
                "no_show"
              ]
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Only bookings whose `start_time` is on or after this calendar day, on each booking's room's clock.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Only bookings whose `end_time` is on or before the end of this calendar day, on each booking's room's clock.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the bookings the caller may read",
            "headers": {
              "X-Total-Count": {
                "description": "Bookings matching the filters, across all pages.",
                "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",
                  "required": [
                    "success",
                    "bookings",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "bookings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BookingsBookingListItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/BookingsPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:bookings` (\"Required scope: read:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business (\"Bookings is not enabled\").\n- `app_not_accessible` — the caller is not an admin or Bookings app admin and the app is not published to them (\"Access denied\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Book a space",
        "description": "Books a space for the caller. The booking is always the caller's own\n(`user` is the token's person), is created `confirmed`, and gets a\ngenerated `booking_reference` (`API<business id><YYYYMMDD><8 hex>`).\n\nThe tenant's rules apply as on the web: \"Who Can Book\" (403\n`booking_not_permitted`), the booking policy (daily cap, advance\nwindow, bookable hours, minimum / maximum duration), space types the\ntenant switched off, the space's capacity, no booking in the past, and\nno overlap with another confirmed booking of the same space (\"Space is\nnot available during the selected time\"). Each is answered `422\nvalidation_failed` with the sentence in `message` and every sentence in\n`details.messages`. A `space_id` that is not one of this business's\nspaces is dropped, so the save fails \"Space can't be blank\".\n\nTimes: a naive time is read in `time_entry_zone` when it names a valid\ntime zone, else on the chosen space's clock. On success the holder is\nsent the confirmation (mail with a calendar invite and an in-app\nnotification), as the web door does.\n\n**Required Scopes:** `write:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:bookings\")\nbefore anything else is checked. The app's sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingsBookingWrite"
              },
              "example": {
                "booking": {
                  "space_id": 42,
                  "start_time": "2026-10-14T14:00",
                  "end_time": "2026-10-14T15:00",
                  "purpose": "Planning",
                  "attendee_count": 4
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The booking was made",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking": {
                      "$ref": "#/components/schemas/BookingsBookingCreated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `booking` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:bookings` (\"Required scope: write:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `booking_not_permitted` — the tenant's \"Who Can Book\" policy does not include the caller (\"Booking permissions required.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` — the booking could not be saved; `details.messages` lists every reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/bookings/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The booking's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Get a booking",
        "description": "One booking, with its notes, check-in times, cost and space location.\nFound only among the bookings the caller may read (the same rule as\nthe list); any other id answers `404 not_found` (\"Booking not found\").\n\n**Required Scopes:** `read:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:bookings\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The booking",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking": {
                      "$ref": "#/components/schemas/BookingsBookingDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:bookings` (\"Required scope: read:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Bookings"
        ],
        "summary": "Change a booking",
        "description": "Changes a booking's space, times, purpose, notes, attendee count or\nattendees. Send only the fields to change, inside `booking`.\n\nWho may: the holder, or someone the tenant's \"Who can see everyone's\nbookings?\" policy lets act on the holder (on the default `managers`\npolicy, the holder's own manager) — anyone else gets `403 forbidden`\n(\"Not authorized\"). The caller must also pass \"Who Can Book\" (403\n`booking_not_permitted`, checked before the booking is looked up).\n\nThe record's own rules: a booking that is not `confirmed`, or has\nstarted, keeps every field (`422 not_editable`, with the reason —\ncancelled, ended or started), as on the booking page and the phone.\nThe space can no longer be changed (`422 space_locked`) and the times\ncan no longer be changed (`422 time_locked`) once the booking is\nchecked in or starts within the tenant's modification window (default\n2 hours). The booking policy is then re-run on the\nchanged booking (daily cap, advance window, bookable hours, overlap)\nand a failure answers `422 validation_failed`.\n\nA naive time is read in `time_entry_zone` when valid, else on the clock\nof the space the booking moves to (or its current space). When the\ntime or the space actually changed, the holder is sent the corrected\ncalendar invite; a purpose-only change sends nothing.\n\n**Required Scopes:** `write:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:bookings\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingsBookingWrite"
              },
              "example": {
                "booking": {
                  "purpose": "Quarterly planning"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The booking was changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking": {
                      "$ref": "#/components/schemas/BookingsBookingUpdated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `booking` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:bookings` (\"Required scope: write:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `booking_not_permitted` — the tenant's \"Who Can Book\" policy does not include the caller.\n- `forbidden` — the caller may read the booking but may not change it (\"Not authorized\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `not_editable` — the booking was cancelled, has ended or has started (\"This booking has ended, so its details can no longer be changed.\").\n- `space_locked` — \"This booking's space can no longer be changed.\"\n- `time_locked` — \"This booking's time can no longer be changed.\"\n- `validation_failed` — the changed booking could not be saved; `details.messages` lists every reason.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Bookings"
        ],
        "summary": "Change a booking (PUT)",
        "description": "Same as PATCH: the same action, rules, body and responses. Send only\nthe fields to change, inside `booking`.\n\n**Required Scopes:** `write:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:bookings\").\nA browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingsBookingWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The booking was changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking": {
                      "$ref": "#/components/schemas/BookingsBookingUpdated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `booking` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:bookings`.\n- `app_not_enabled`, `app_not_accessible`, `booking_not_permitted`, `forbidden` — as for PATCH.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_editable`, `space_locked`, `time_locked` or `validation_failed` — as for PATCH.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Bookings"
        ],
        "summary": "Delete a booking",
        "description": "Deletes the booking outright — the row is removed and the space is\nfreed. To release a booking but keep it in the holder's history (and\nin the utilization and cancellation counts), use\n`POST /bookings/bookings/{id}/cancel` instead.\n\nWho may: the holder, or someone the tenant's \"Who can see everyone's\nbookings?\" policy lets act on the holder; anyone else gets `403\nforbidden` (\"Not authorized\"). Any status may be deleted, but not\ninside the tenant's cancellation window (default 2 hours before the\nstart) — which also covers every booking that has already started —\nanswered `422 inside_cancellation_window` (\"Bookings can only be\ndeleted N hours in advance.\"). A walk-up hold made on a room display\nhas no window.\n\nThe holder is sent the cancellation (which withdraws the calendar\ninvite), and a \"booking cancelled\" entry naming who deleted it is\nwritten to the app's activity trail.\n\n**Required Scopes:** `destructive:bookings` — `write:bookings` is not\nenough. A token without it gets `403 insufficient_permissions`\n(\"Required scope: destructive:bookings\") before anything else is\nchecked. The app's sign-in tokens hold it; a browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The booking was deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:bookings` (\"Required scope: destructive:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `forbidden` — the caller may read the booking but may not delete it (\"Not authorized\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `held_by_meeting` — a Calendar meeting holds this room for this date; it is changed or released from the meeting, and the message says by whom.\n- `inside_cancellation_window` — the booking starts within the tenant's cancellation window, or has already started.\n- `validation_failed` — the delete was refused (\"Unable to delete booking.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/bookings/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The booking's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Cancel a booking",
        "description": "Cancels the booking and KEEPS it, with status `cancelled`, so the\nholder's history, the waitlist and the utilization and cancellation\ncounts still see it. The space is freed for the slot. No body.\n\nWho may: the holder, or someone the tenant's \"Who can see everyone's\nbookings?\" policy (Bookings → Settings → User Permissions) lets act on\nthe holder; anyone else gets `403 forbidden` (\"Not authorized\"). Only a\n`confirmed` booking can be cancelled (`422 not_cancellable`, e.g.\n\"This booking is already cancelled and can't be cancelled.\"), not once\nit has ended (`422 booking_ended`), and not inside the tenant's\ncancellation window (default 2 hours before the start): `422\ninside_cancellation_window` (\"Bookings can only be cancelled N hours in\nadvance.\"). A business admin or Bookings app admin may cancel inside\nthe window — unless someone is checked in to the booking, which is\nreleased by checking them out instead (`422 checked_in`).\n\nThe holder is sent the cancellation, which withdraws the calendar\ninvite.\n\n**Required Scopes:** `destructive:bookings` — cancelling is a\ndelete-level act, so `write:bookings` is not enough. A token without it\ngets `403 insufficient_permissions` (\"Required scope:\ndestructive:bookings\") before anything else is checked. The app's\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The booking was cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "booking"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "booking": {
                      "$ref": "#/components/schemas/BookingsBookingCreated"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:bookings` (\"Required scope: destructive:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `forbidden` — the caller may read the booking but may not cancel it (\"Not authorized\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `not_cancellable` — the booking is not `confirmed` (already cancelled, completed or a no-show).\n- `booking_ended` — the booking has already ended, so there's nothing to cancel.\n- `held_by_meeting` — a Calendar meeting holds this room for this date; it is changed or released from the meeting. A business admin or Bookings app admin may release the one date (the meeting stays, and its host is told); the message says who can act.\n- `checked_in` — someone is checked in to the booking; checking them out releases it.\n- `inside_cancellation_window` — the booking starts within the tenant's cancellation window (an admin is not bound by it).\n- `validation_failed` — the cancellation could not be saved; `details.messages` lists why.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/bookings/{id}/check_in": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The booking's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Check in to a booking",
        "description": "Marks the booking checked in now (`checked_in: true`,\n`checked_in_at`). No body.\n\nWho may: the holder, someone the tenant's \"Who can see everyone's\nbookings?\" policy lets act on the holder, or the tenant's facilities staff;\nanyone else gets `403 forbidden` (\"Not authorized\"). Check-in is open\nto a `confirmed`, not yet checked-in booking from 15 minutes before its\nstart until its end; otherwise `422 cannot_check_in` (\"Cannot check\nin at this time\").\n\n**Required Scopes:** `write:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:bookings\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Checked in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsActionResult"
                },
                "example": {
                  "success": true,
                  "message": "Checked in successfully"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:bookings` (\"Required scope: write:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `forbidden` — the caller may read the booking but may not check it in (\"Not authorized\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `cannot_check_in` — outside the check-in window, already checked in, or not `confirmed`.\n- `validation_failed` — the check-in could not be saved; `details.messages` lists why.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/bookings/{id}/check_out": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The booking's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Check out of a booking",
        "description": "Records the departure now (`checked_out_at`) and completes the booking\n(status `completed`), freeing the space for the rest of the slot. No\nbody. A \"check out\" entry is written to the app's activity trail and\nthe holder is asked to rate the space.\n\nWho may: as for check-in. Check-out is open to a checked-in booking not\nyet checked out, from its start until 30 minutes after its end;\notherwise `422 cannot_check_out` (\"Cannot check out at this time\").\n\n**Required Scopes:** `write:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:bookings\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Checked out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsActionResult"
                },
                "example": {
                  "success": true,
                  "message": "Checked out successfully"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:bookings` (\"Required scope: write:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n- `forbidden` — the caller may read the booking but may not check it out (\"Not authorized\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no booking with this id that the caller may read (\"Booking not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `cannot_check_out` — not checked in, already checked out, before the start, or more than 30 minutes after the end.\n- `validation_failed` — the check-out could not be saved; `details.messages` lists why.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingsValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/spaces": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "List bookable spaces",
        "description": "The business's active spaces (rooms, desks, parking spots and other\nresources) ordered by name (then id), paginated — the list an\nintegrator books from. Spaces of a type the tenant switched off in\nBookings settings are left out, as the booking pickers leave them out.\n\nFilters (each optional):\n- `type` — one space type, or several as `type[]=desk&type[]=phone_booth`.\n- `building` — one building name, or several as `building[]=`.\n- `min_capacity` — spaces holding at least this many people; a\n  non-numeric value reads as 0.\n  A hash-shaped `type` or `building` is ignored.\n\nThe response also carries `X-Total-Count`, `X-Total-Pages`,\n`X-Current-Page` and `X-Per-Page` headers.\n\n**Required Scopes:** `read:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:bookings\")\nbefore anything else is checked. The app's sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Spaces per page. A numeric value is clamped to 1..100; a blank or non-numeric value means 25.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Only spaces of this type. Repeat as `type[]=` for several.",
            "schema": {
              "type": "string",
              "enum": [
                "desk",
                "meeting_room",
                "parking_spot",
                "locker",
                "phone_booth",
                "av_equipment",
                "company_vehicle",
                "gym_equipment"
              ]
            }
          },
          {
            "name": "building",
            "in": "query",
            "description": "Only spaces in this building (exact match). Repeat as `building[]=` for several.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "min_capacity",
            "in": "query",
            "description": "Only spaces whose capacity is at least this number.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the business's active spaces",
            "headers": {
              "X-Total-Count": {
                "description": "Spaces matching the filters, across all pages.",
                "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",
                  "required": [
                    "success",
                    "spaces",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "spaces": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BookingsSpaceListItem"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/BookingsPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:bookings` (\"Required scope: read:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business (\"Bookings is not enabled\").\n- `app_not_accessible` — the caller is not an admin or Bookings app admin and the app is not published to them (\"Access denied\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/spaces/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The space's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Get a space",
        "description": "One space of the business, with its description, amenities and\nequipment. Any space of the business is returned — including an\ninactive one or one of a type the tenant switched off, which the list\nleaves out. An id from another business answers `404 not_found`\n(\"Space not found\").\n\n**Required Scopes:** `read:bookings`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:bookings\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The space",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "space"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "space": {
                      "$ref": "#/components/schemas/BookingsSpace"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:bookings` (\"Required scope: read:bookings\").\n- `app_not_enabled` — Bookings is not enabled for the business.\n- `app_not_accessible` — the caller may not open Bookings (\"Access denied\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no space with this id in the business (\"Space not found\"), or the Bookings app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/manifest.xml": {
      "get": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Get the Office add-in manifest",
        "description": "The add-in's XML manifest (an Office `TaskPaneApp` for Word documents\nand Excel workbooks), for a Microsoft 365 admin to deploy the add-in\nfrom the Integrated Apps panel. Public — no authentication, no scope.\n\nThe manifest carries no tenant data. Its `SourceLocation`\n(`/office-addin/taskpane.html`), icon URLs and `AppDomain` are built\nfrom the host the request was sent to, so each workspace address\nserves a manifest pointing at itself.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The add-in manifest",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<OfficeApp xmlns=\"http://schemas.microsoft.com/office/appforoffice/1.1\" xsi:type=\"TaskPaneApp\">\n  <Id>e4b1f04a-1e7b-4f1d-9a4e-a6d0c8e7f001</Id>\n  <Version>1.0.0.0</Version>\n  <ProviderName>MangoApps</ProviderName>\n  <DisplayName DefaultValue=\"MangoApps for Microsoft 365\"/>\n  <Hosts><Host Name=\"Document\"/><Host Name=\"Workbook\"/></Hosts>\n  <DefaultSettings>\n    <SourceLocation DefaultValue=\"https://acme.workforce.mangoapps.com/office-addin/taskpane.html\"/>\n  </DefaultSettings>\n  <Permissions>ReadWriteDocument</Permissions>\n</OfficeApp>\n"
              }
            }
          }
        }
      }
    },
    "/office_integration/apps": {
      "get": {
        "tags": [
          "Office Integration"
        ],
        "summary": "List the apps for the add-in's picker",
        "description": "The record types the add-in may offer this person in this Office host:\nevery registered adapter that is enabled for the business (its app\nenabled, and the app's `office_integration_enabled` setting not off —\nLibraries only when a tenant switched it on; the App attachments and\nSafety Hub incident types are never offered), that supports `host`\nwhen one is sent, and whose `read:<area>` scope the token holds.\n\nOne entry per record type. Its `slug` is the record type's registry\nkey — the `{app_slug}` every records path takes (the table under `GET\n/office_integration/records/{app_slug}/search`) — and `app_slug` is\nthe marketplace app it belongs to, which several types share\n(`service-desk` has its knowledge base, help requests, change\nrequests and their comments). An app with several types names each\none: `name` is \"<App> — <record type>\" (e.g. \"Service Desk — Help\nrequests\"); an app with one type is just its name.\n\n`capabilities.save_revision` says whether the type can save an edited\ndocument back (`POST …/revisions`); the add-in offers \"Save revision\"\nonly where it is `true`.\n\n**Required Scopes:** the `read:<area>` scope of at least one Office\napp — `read:asset_pro`, `read:ballots`, `read:contracts`, `read:drive`,\n`read:epms_goals`, `read:field_service_suite`, `read:ideas`,\n`read:job_board`, `read:libraries`, `read:okr`, `read:onboarding`,\n`read:policy_hub`, `read:procurement`, `read:safety_hub`,\n`read:service_desk`, `read:sop_hub`, `read:tasks`, `read:training` or\n`read:wikis`. A token holding none of them gets `403\ninsufficient_permissions` (\"Required scope: read:<area> of an Office\napp (e.g. read:drive)\") instead of an empty picker. Each app is then\nlisted only when its own read scope is held. A browser session is\nasked no scope and sees every enabled app.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "description": "The Office host the pane runs in. Only apps supporting it are listed; any other value lists none. Omit it to list every enabled app.",
            "schema": {
              "type": "string",
              "enum": [
                "word",
                "excel"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The apps to offer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "apps"
                  ],
                  "properties": {
                    "apps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfficeIntegrationApp"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` — the token holds no Office app's read scope (\"Required scope: read:<area> of an Office app (e.g. read:drive)\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/records/{app_slug}/search": {
      "get": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Search an app's records",
        "description": "Finds records of one record type that this person may open, for the\npane's record picker. A blank `q` returns the first `limit` records;\notherwise the adapter's own search applies (by default a\ncase-insensitive substring match on the record's title). No\npagination — at most `limit` results.\n\nRegistry keys (`{app_slug}`), the record type each opens, and the\nscope area it asks:\n\n| `{app_slug}` | Record type | Area |\n|---|---|---|\n| `contracts` | Contract documents | `contracts` |\n| `contracts-vendor-certification` | Vendor certifications | `contracts` |\n| `policy-hub` | Policies | `policy_hub` |\n| `sop-hub` | SOPs | `sop_hub` |\n| `job-board` | Offer letters | `job_board` |\n| `job-board-kb` | Job Board knowledge-base files | `job_board` |\n| `job-board-assessment` | Assessment templates | `job_board` |\n| `drive` | Drive files | `drive` |\n| `app-attachment` | Broadcast-comment, Workspace and DM attachments (never enabled here) | `drive` |\n| `procurement` | Purchase requisitions (Excel) | `procurement` |\n| `field-service-suite` | Estimates (Excel) | `field_service_suite` |\n| `libraries` | Library files | `libraries` |\n| `ballots` | Ballot item documents | `ballots` |\n| `onboarding-hub` | Onboarding document library | `onboarding` |\n| `onboarding-hub-task` | Onboarding task files | `onboarding` |\n| `training` | Training lessons | `training` |\n| `training-custom-field` | Training custom-field files | `training` |\n| `ideas` | Ideas | `ideas` |\n| `tasks-task` | Tasks | `tasks` |\n| `tasks-template` | Task templates | `tasks` |\n| `wikis-page` | Wiki pages | `wikis` |\n| `wikis-comment` | Wiki comments | `wikis` |\n| `epms-goal` | Goals | `epms_goals` |\n| `okr-objective` | OKR objectives | `okr` |\n| `okr-checkin` | OKR check-ins | `okr` |\n| `asset-pro` | Assets | `asset_pro` |\n| `safety-hub-kb` | Safety Hub knowledge-base files | `safety_hub` |\n| `safety-hub-contractor` | Contractor prequalifications | `safety_hub` |\n| `safety-hub-incident` | Incidents (never enabled here) | `safety_hub` |\n| `safety-hub-topic` | Toolbox talk topics | `safety_hub` |\n| `service-desk-kb` | Service Desk knowledge-base files | `service_desk` |\n| `service-desk-help` | Help requests | `service_desk` |\n| `service-desk-change` | Change requests | `service_desk` |\n| `service-desk-comment` | Help request comments | `service_desk` |\n\n**Required Scopes:** `read:<area>` of the record type (the table). A\ntoken without it gets `403 insufficient_permissions` (\"Required scope:\nread:<area>\"), after the key and the app's enablement are checked. An\nown_ form does not count. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "app_slug",
            "in": "path",
            "required": true,
            "description": "The record type's registry key (see the table on the search operation), e.g. `contracts` or `service-desk-help`. Lowercase letters, digits and hyphens only.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search. Blank returns the first `limit` records.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Most results to return. A value of 0 or less (or non-numeric) means 25; above 100 means 100.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "X-Office-Host",
            "in": "header",
            "required": false,
            "description": "The Office host the pane runs in (`word` / `excel`). Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Addin-Version",
            "in": "header",
            "required": false,
            "description": "The add-in's version. Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching records",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "results",
                    "count"
                  ],
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfficeIntegrationSearchResult"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "The number of entries in `results`."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `app_disabled` — the record type is not enabled for the business (\"Office integration is not enabled for '<app_slug>'\").\n- `insufficient_permissions` — the token lacks the type's `read:<area>` (\"Required scope: read:<area>\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`unknown_app` — no record type is registered under `{app_slug}` (\"No Office adapter registered for '<app_slug>'\"). For a marketplace slug whose types have their own keys (`service-desk`) the message lists them: \"… Its record types are addressed as: service-desk-kb, service-desk-help, …\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/records/{app_slug}/{id}": {
      "get": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Open a record in the task pane",
        "description": "What the pane shows beside the open document: the record's title,\nsubtitle, status, fields, the actions this person may take, the\nactions they would expect but cannot take yet (`blocked_actions`, each\nwith the reason), AI insights, the record's web page and its current\n`etag` — send that `etag` as `If-Match` when saving a revision —\nand `capabilities.save_revision`, whether the record type can save\none at all.\n\nAn action the token's scopes do not cover is moved from `actions` to\n`blocked_actions` with the reason \"This token doesn't include <scope>.\nSign out and choose Sign in to do this here, or open it in MangoApps.\"\n— a blocked action is information only, and posting it is refused.\n\n**Required Scopes:** `read:<area>` of the record type (see the search\noperation's table). A token without it gets `403\ninsufficient_permissions` (\"Required scope: read:<area>\"). A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "app_slug",
            "in": "path",
            "required": true,
            "description": "The record type's registry key (see the table on the search operation), e.g. `contracts` or `service-desk-help`. Lowercase letters, digits and hyphens only.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record's id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "X-Office-Host",
            "in": "header",
            "required": false,
            "description": "The Office host the pane runs in (`word` / `excel`). Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Addin-Version",
            "in": "header",
            "required": false,
            "description": "The add-in's version. Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The record's pane payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfficeIntegrationRecord"
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `app_disabled` — the record type is not enabled for the business.\n- `insufficient_permissions` — the token lacks the type's `read:<area>` (\"Required scope: read:<area>\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `unknown_app` — no record type is registered under `{app_slug}`.\n- `not_found` — no such record, or this person may not open it (\"Record not found or not visible to user\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/records/{app_slug}/{id}/file": {
      "get": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Download a record's document",
        "description": "The record's document (`.docx`, `.xlsx` or the stored file) as an\nattachment, for the pane to open in Word or Excel. The\n`X-Office-File-ETag` response header carries the stored file's\nchecksum when there is one.\n\nWhen the record's app asks for identity verification before a\ndownload (Contracts' step-up setting), a token caller is refused `403\nstep_up_required` with the record's web page in `details.web_url`,\nwhere the person can verify; a web session already verified for that\nscope is let through. A document over 50 MB is refused `413\nfile_too_large` — open it in the web app instead. A record with no\ndocument answers `404 not_found`.\n\n**Required Scopes:** `read:<area>` of the record type (see the search\noperation's table). A token without it gets `403\ninsufficient_permissions` (\"Required scope: read:<area>\"). A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "app_slug",
            "in": "path",
            "required": true,
            "description": "The record type's registry key (see the table on the search operation), e.g. `contracts` or `service-desk-help`. Lowercase letters, digits and hyphens only.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record's id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "X-Office-Host",
            "in": "header",
            "required": false,
            "description": "The Office host the pane runs in (`word` / `excel`). Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Addin-Version",
            "in": "header",
            "required": false,
            "description": "The add-in's version. Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"<the document's filename>\"`",
                "schema": {
                  "type": "string"
                }
              },
              "X-Office-File-ETag": {
                "description": "The stored file's checksum, when the record type provides one.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `app_disabled` — the record type is not enabled for the business.\n- `insufficient_permissions` — the token lacks the type's `read:<area>` (\"Required scope: read:<area>\").\n- `step_up_required` — the tenant requires identity verification to open this file; `details` is `{ \"requires_step_up\": true, \"scope\": \"<step-up scope>\", \"web_url\": \"<the record's web page>\" }`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `unknown_app` — no record type is registered under `{app_slug}`.\n- `not_found` — no such record, this person may not open it, or it has no document (\"Record not found or not visible to user\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`file_too_large` — the document is over 50 MB (\"Document exceeds the 50 MB Office-edit limit. Open it in the web app instead.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/records/{app_slug}/{id}/revisions": {
      "post": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Save a new revision of a record's document",
        "description": "Uploads the edited document as the record's new revision. Where the\nrevision lands is the record type's own rule (Contracts' revision\nhistory, a Drive file version, a policy or SOP's content, an offer\nletter, a requisition's or estimate's line items read from the sheet).\n\nConflict check: send the `etag` from `GET\n/office_integration/records/{app_slug}/{id}` as `If-Match`. When the\nrecord changed since, nothing is saved and the answer is `409\netag_conflict` with both tags in `details`.\n\nA record type may REFUSE the save (a non-draft record, no rows read\nfrom the sheet, row errors, no text): that is answered `200` with\n`success: false` and the sentence in `error` — read `success`, not the\nstatus. A save that fails unexpectedly is `422 save_failed`.\n\nOnly Contracts (`contracts`), Policy Hub, SOP Hub, Job Board offer\nletters (`job-board`), Drive, Procurement and Field Service save a\nrevision here — the types whose `capabilities.save_revision` is\n`true`. Every other record type answers `422\nsave_revision_unsupported` (\"Saving revisions isn't supported for\n<App> — <record type>\") before the record is looked up, and the\nrefusal is audited; its documents are edited in MangoApps (or in\nMicrosoft 365 where the tenant turned that on).\n\n**Required Scopes:** `write:<area>` of the record type (see the search\noperation's table). A token without it gets `403\ninsufficient_permissions` (\"Required scope: write:<area>\"). A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "app_slug",
            "in": "path",
            "required": true,
            "description": "The record type's registry key (see the table on the search operation), e.g. `contracts` or `service-desk-help`. Lowercase letters, digits and hyphens only.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record's id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "description": "The record's `etag` when the document was opened; a mismatch answers 409 and saves nothing.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Host",
            "in": "header",
            "required": false,
            "description": "The Office host the pane runs in (`word` / `excel`). Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Addin-Version",
            "in": "header",
            "required": false,
            "description": "The add-in's version. Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The edited document. At most 50 MB."
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional note recorded with the revision."
                  },
                  "filename": {
                    "type": "string",
                    "description": "Used only when the file part carries no filename; defaults to `document.docx`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The record type's answer. `success` is `true` unless the record\ntype refused the save, in which case it is `false` and `error`\nsays why.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfficeIntegrationRevisionResult"
                }
              }
            }
          },
          "400": {
            "description": "- `missing_file` — no multipart `file` part (\"Multipart file part is required\").\n- `missing_size` — the upload's size could not be determined.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `app_disabled` — the record type is not enabled for the business.\n- `insufficient_permissions` — the token lacks the type's `write:<area>` (\"Required scope: write:<area>\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `unknown_app` — no record type is registered under `{app_slug}`.\n- `not_found` — no such record, or this person may not open it (\"Record not found or not visible to user\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`etag_conflict` — the record changed since it was opened; `details` is `{ \"current_etag\", \"your_etag\" }`. Nothing was saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`file_too_large` — the upload is over 50 MB (\"Maximum revision size is 50 MB\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `save_revision_unsupported` — the record type cannot save a revision (\"Saving revisions isn't supported for <App> — <record type>\").\n- `save_failed` — the save raised an error (\"Saving the revision failed. Please try again or open the document in MangoApps.\"); the detail is in the audit log.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/office_integration/records/{app_slug}/{id}/actions/{name}": {
      "post": {
        "tags": [
          "Office Integration"
        ],
        "summary": "Take an action on a record",
        "description": "Takes one of the record's actions — only a `name` listed in the\nrecord's `actions` (from `GET /office_integration/records/{app_slug}/{id}`)\nis accepted; anything else, including a `blocked_actions` entry, is\n`403 action_not_allowed`. The actions offered today:\n\n- `contracts`: `open_web`, `submit_for_approval`, `approve`, `reject`, `send_for_signature`\n- `procurement`: `open_web`, `submit`, `approve`, `reject`\n- `policy-hub`, `sop-hub`: `open_web`, `publish`, `unpublish`\n- `job-board` (offer letters): `open_web`, `submit_for_approval`, `send_to_candidate`\n- `field-service-suite`: `open_web`, `send_to_customer`\n- `drive`: `open_web`\n- every other record type: none\n\nThe JSON body is handed to the record type's action (routing keys and\n`user_id`, `business_id`, `approver_id`, `token`, `access_token`,\n`api_key` and `authenticity_token` are stripped). `reject` takes a\nrequired `reason` (Contracts also reads `comments`): a blank one is\nrefused `422`.\n\nThe answer is the action's result: `200` with `success: true`, or\n`422` when the action refused — the api/v1 error envelope (its `error`\nsentence as `error.message`) with `success: false` and any other keys\nthe action returned. An action that raises is `500 action_failed`.\n\n**Required Scopes**, by action:\n- `open_web` (changes nothing) — `read:<area>` of the record type.\n- `reject` on `contracts` — `destructive:contracts`; on `procurement`\n  — `destructive:procurement`. The delete scope alone is asked (not\n  the write scope as well), and only after the record is found and\n  the action is confirmed to be one it offers.\n- every other action — `write:<area>` of the record type.\n\nA token without the scope gets `403 insufficient_permissions`\n(\"Required scope: <scope>\"). See the search operation for each type's\narea. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "app_slug",
            "in": "path",
            "required": true,
            "description": "The record type's registry key (see the table on the search operation), e.g. `contracts` or `service-desk-help`. Lowercase letters, digits and hyphens only.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record's id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The action's name, from the record's `actions`. Lowercase letters, digits, `_` and `-`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9_-]+$"
            },
            "example": "approve"
          },
          {
            "name": "X-Office-Host",
            "in": "header",
            "required": false,
            "description": "The Office host the pane runs in (`word` / `excel`). Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Office-Addin-Version",
            "in": "header",
            "required": false,
            "description": "The add-in's version. Recorded in the audit log only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Required by `reject`: why it is rejected. The requester or uploader sees it. Capped by the action's `reason_max`."
                  }
                }
              },
              "example": {
                "reason": "Duplicate of REQ-0042"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The action was taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfficeIntegrationActionResult"
                },
                "example": {
                  "success": true,
                  "message": "Rejected"
                }
              }
            }
          },
          "401": {
            "description": "- Not authenticated (no or an invalid bearer token).\n- `authentication_required` — the request resolved no user or business (\"Office Add-in requires authenticated user + business\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `app_disabled` — the record type is not enabled for the business.\n- `insufficient_permissions` — the token lacks the scope the action asks (\"Required scope: <scope>\").\n- `action_not_allowed` — the action is not one the record offers this person right now (\"Action '<name>' is not available for this record\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `unknown_app` — no record type is registered under `{app_slug}`.\n- `not_found` — no such record, or this person may not open it (\"Record not found or not visible to user\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The action refused (e.g. \"A rejection reason is required — the requester is notified with it.\"). The api/v1 error envelope — `error.code` is the action's own `code` when it returned one, else `validation_failed` — beside `success: false` and the action's other keys.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "enum": [
                            false
                          ]
                        }
                      },
                      "additionalProperties": true
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`action_failed` — the action raised (\"Action failed unexpectedly. The error has been logged.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/smart_defaults": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get the caller's AI-assistant smart defaults",
        "description": "The caller's learned AI-assistant defaults for this business\n(SmartDefaultsEngineService#get_smart_defaults): preferred assistant,\ncommunication style, complexity, session length, plus keys added for\nthe time of day, the `category`, the `assistant_type` and the caller's\nrecent workload, and a confidence score with its factors. When the\ncaller has no row in the `smart_defaults_calculation` view the base\nvalues are the fallback set (`source: fallback_defaults`).\n\n`assistant_type` adds its own keys: `task_assistant`,\n`form_assistant` (also `field_assistance_level`),\n`database_assistant` (also `query_complexity`) and `admin_assistant`\n(also `safety_level`) set `focus_areas`, `suggested_prompts` and\n`interaction_style`; any other value is accepted and adds none.\n\n**Rate limit:** this endpoint and `contextual_suggestions` share one\ncounter per user of 100 calls; the counter expires an hour after the\nlast call it counted. Over the limit the call answers\n`429 rate_limited` with `retry_after: 3600`.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntelligenceAssistantType"
          },
          {
            "name": "category",
            "in": "query",
            "description": "A category to tailor to. When the caller has interaction patterns in it with a success rate of 70 or more, `category_complexity`, `category_duration`, `category_style` and `category_confidence` are added.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "context",
            "in": "query",
            "description": "Extra context as a JSON-encoded object string, or as a nested query hash (`context[a]=b`). A string that is not a valid JSON object is treated as `{}`.",
            "schema": {
              "type": "string",
              "example": "{\"source\":\"dashboard\"}"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's smart defaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "defaults",
                    "generated_at",
                    "expires_at",
                    "metadata"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "defaults": {
                      "$ref": "#/components/schemas/IntelligenceSmartDefaults"
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "One hour after `generated_at`."
                    },
                    "metadata": {
                      "type": "object",
                      "required": [
                        "user_id",
                        "business_id",
                        "assistant_type",
                        "category"
                      ],
                      "properties": {
                        "user_id": {
                          "type": "integer"
                        },
                        "business_id": {
                          "type": "integer"
                        },
                        "assistant_type": {
                          "type": "string"
                        },
                        "category": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IntelligenceRateLimited"
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate smart defaults\", with `fallback_defaults` — the built-in defaults for the requested `assistant_type` — beside it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "fallback_defaults": {
                          "type": "object",
                          "properties": {
                            "ai_assistant": {
                              "type": "string"
                            },
                            "communication_style": {
                              "type": "string",
                              "example": "moderate"
                            },
                            "complexity_preference": {
                              "type": "string",
                              "example": "moderate"
                            },
                            "session_duration": {
                              "type": "string",
                              "example": "medium"
                            },
                            "preferred_categories": {
                              "type": "array",
                              "items": {}
                            },
                            "peak_usage_hours": {
                              "type": "array",
                              "items": {}
                            },
                            "confidence_score": {
                              "type": "number",
                              "example": 0.1
                            },
                            "source": {
                              "type": "string",
                              "example": "fallback_defaults"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/contextual_suggestions": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get contextual suggestions for an ongoing AI conversation",
        "description": "What the person might send next in an AI-assistant conversation\n(ContextAwareResponseService#get_contextual_suggestions), ranked by\n`combined_score` and cut to `max_suggestions`:\n\n- the approaches and response templates recorded on the caller's OWN\n  successful interaction patterns (success rate 70+, confidence\n  60+) — never another person's;\n- follow-ups for the intent of the latest turn (the `input`, after\n  the `conversation_state` messages), one toward `target_outcome`\n  when it is given, and one when the person signals urgency.\n\nWith no conversation and no learned patterns the list still holds the\ngeneral follow-ups.\n\n**Rate limit:** shares the 100-call per-user counter with\n`smart_defaults` (see there); over it, `429 rate_limited`.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "input",
            "in": "query",
            "description": "The text the person is typing or has just sent.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "conversation_state",
            "in": "query",
            "description": "The conversation so far as a JSON-encoded object string, `{\"messages\":[{\"role\":\"user\",\"content\":\"…\"}]}` — its last 20 messages are read. A string that is not a valid JSON object, or one without a `messages` array, reads as no conversation.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "target_outcome",
            "in": "query",
            "description": "What the conversation is trying to reach.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "max_suggestions",
            "in": "query",
            "description": "How many suggestions to return, at most 20. Missing, zero, negative or not a whole number reads as 5.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "suggestions",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "suggestions": {
                      "type": "object",
                      "properties": {
                        "suggestions": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/IntelligenceContextualSuggestion"
                          }
                        },
                        "context_confidence": {
                          "type": "number"
                        },
                        "similar_patterns_found": {
                          "type": "integer"
                        },
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IntelligenceRateLimited"
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate suggestions\", with an empty `suggestions` array beside it, when the suggestion service raises.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "suggestions": {
                          "type": "array",
                          "maxItems": 0,
                          "items": {}
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/user_insights": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get the caller's AI-assistant intelligence insights",
        "description": "Everything learned about the caller's AI-assistant use in this\nbusiness (IntelligenceTrackingService#get_user_intelligence_insights):\ntheir smart defaults for `assistant_type`, a behaviour analysis,\nworkflow recommendations from their successful patterns, optimisation\nopportunities, learning progress and confidence metrics — plus the\nheadline numbers of their intelligence profile.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntelligenceAssistantType"
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's insights",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "insights",
                    "user_profile",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "insights": {
                      "type": "object",
                      "properties": {
                        "smart_defaults": {
                          "$ref": "#/components/schemas/IntelligenceSmartDefaults"
                        },
                        "behavior_analysis": {
                          "type": "object",
                          "description": "UserIntelligenceProfile#get_personalization_insights.",
                          "properties": {
                            "intelligence_level": {
                              "type": "string"
                            },
                            "pattern_confidence": {
                              "type": "string"
                            },
                            "interaction_style": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "learning_progress": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "optimization_opportunities": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            },
                            "behavioral_trends": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "prediction_accuracy": {
                              "type": "number"
                            }
                          }
                        },
                        "pattern_recommendations": {
                          "type": "array",
                          "description": "Up to five of the caller's patterns with a success rate of 70+ and confidence of 60+, strongest first.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "workflow_type": {
                                "type": "string"
                              },
                              "success_rate": {
                                "type": "string",
                                "description": "Decimal, as a string."
                              },
                              "confidence": {
                                "type": "string",
                                "description": "Decimal, as a string."
                              },
                              "workflow_steps": {
                                "type": "array",
                                "items": {}
                              },
                              "usage_frequency": {
                                "type": "integer"
                              },
                              "recommendation_strength": {
                                "type": "number"
                              }
                            }
                          }
                        },
                        "optimization_opportunities": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string"
                              },
                              "priority": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "learning_progress": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Empty when it cannot be computed."
                        },
                        "confidence_metrics": {
                          "type": "object",
                          "properties": {
                            "pattern_confidence": {
                              "type": "string",
                              "description": "Decimal, as a string."
                            },
                            "data_sufficiency": {
                              "type": "number"
                            },
                            "prediction_accuracy": {
                              "type": "number"
                            },
                            "feedback_consistency": {
                              "type": "number"
                            }
                          }
                        }
                      }
                    },
                    "user_profile": {
                      "type": "object",
                      "required": [
                        "intelligence_score",
                        "pattern_confidence",
                        "total_interactions",
                        "last_interaction",
                        "satisfaction_score"
                      ],
                      "properties": {
                        "intelligence_score": {
                          "type": "string",
                          "description": "Decimal, as a string; a new profile starts at \"25.0\".",
                          "example": "25.0"
                        },
                        "pattern_confidence": {
                          "type": "string",
                          "description": "Decimal, as a string.",
                          "example": "0.0"
                        },
                        "total_interactions": {
                          "type": "integer"
                        },
                        "last_interaction": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "satisfaction_score": {
                          "type": "string",
                          "nullable": true,
                          "description": "Decimal, as a string."
                        }
                      }
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate user insights\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/business_analytics": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get business-wide AI-assistant analytics (admins)",
        "description": "Business-wide AI-assistant analytics for an admin: how many people\nprefer each assistant, communication style, complexity and session\nlength (from the `smart_defaults_calculation` view), plus weekly\npattern trends over 12 weeks, a user segmentation by intelligence\nscore and optimisation recommendations.\n\nOnly an admin or super admin of the business is answered; anyone else\ngets `403 forbidden` (\"Unauthorized\").\n\n**Required Scopes:** `read:analytics`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:analytics\")\nbefore anything else is checked. Only the sign-in doors mint it, so a\ntoken made by hand never carries an admin's width; `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Business analytics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "analytics",
                    "business_insights",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "analytics": {
                      "type": "object",
                      "description": "One aggregate row: `active_users`, `avg_confidence`, `task_assistant_users`, `form_assistant_users`, `database_assistant_users`, `detailed_style_users`, `moderate_style_users`, `concise_style_users`, `high_complexity_users`, `moderate_complexity_users`, `low_complexity_users`, `long_session_users`, `medium_session_users`, `short_session_users`, `common_peak_hours_count`, `avg_hours_since_calculation`.",
                      "additionalProperties": true
                    },
                    "business_insights": {
                      "type": "object",
                      "description": "The same aggregate row plus `pattern_trends` (rows of `week`, `pattern_type`, `pattern_count`, `avg_success_rate`, `avg_confidence`), `user_segmentation` (rows of `user_segment`, `user_count`, `avg_intelligence_score`, `avg_pattern_confidence`, `avg_interactions`, highest score first) and `optimization_recommendations` (objects of `type`, `priority`, `description`, `action`).",
                      "additionalProperties": true
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:analytics` (\"Required scope: read:analytics\").\n- `forbidden` — \"Unauthorized\": the caller is not an admin or super admin of the business.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate business analytics\", when an analytics query raises.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/feedback": {
      "post": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Send feedback on an AI-assistant interaction",
        "description": "Records the caller's feedback on an AI-assistant answer so their\ndefaults can learn from it, and returns improvement suggestions read\nfrom the feedback flags.\n\nThe feedback adapts the caller's profile: `communication_feedback`\n(`too_detailed` → concise, `too_concise` → detailed) and\n`complexity_feedback` (`too_complex` / `too_simple`) move their\ndefaults, and `just_right` answers raise the confidence in them.\n\nA `session_id` the caller has not used before starts a tracking\nsession for them under that id (one row per id — a later call with\nthe same id finds it), and the feedback is also learned against the\ndefaults that session started with. Only the caller's own sessions\nare ever found by id; an id another person's session already holds\nstarts the caller a session under a fresh id instead.\n\n**Required Scopes:** `write:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:chat\") before\nanything else is checked. Its own_ form `write:own_chat` and `admin`\nalso pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "feedback_type": {
                    "type": "string",
                    "description": "What kind of feedback this is. Not validated; the code names `satisfaction`, `preference_change` and `error_report`.",
                    "example": "satisfaction"
                  },
                  "feedback_data": {
                    "type": "object",
                    "description": "The feedback itself, as a JSON object (or a JSON-encoded object string). Three flags produce improvement suggestions when they are JSON `true`.",
                    "properties": {
                      "communication_feedback": {
                        "type": "string",
                        "enum": [
                          "too_detailed",
                          "too_concise",
                          "just_right"
                        ]
                      },
                      "complexity_feedback": {
                        "type": "string",
                        "enum": [
                          "too_complex",
                          "too_simple",
                          "just_right"
                        ]
                      },
                      "communication_too_complex": {
                        "type": "boolean"
                      },
                      "needed_more_examples": {
                        "type": "boolean"
                      },
                      "too_slow": {
                        "type": "boolean"
                      }
                    },
                    "additionalProperties": true
                  },
                  "session_id": {
                    "type": "string",
                    "description": "The AI-assistant session the feedback is about."
                  },
                  "assistant_type": {
                    "type": "string",
                    "default": "task_assistant"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "improvement_suggestions",
                    "recorded_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback recorded successfully"
                    },
                    "improvement_suggestions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "area": {
                            "type": "string",
                            "enum": [
                              "communication_simplification",
                              "example_provision",
                              "response_speed"
                            ]
                          },
                          "action": {
                            "type": "string"
                          },
                          "priority": {
                            "type": "string",
                            "enum": [
                              "high",
                              "medium"
                            ]
                          }
                        }
                      }
                    },
                    "recorded_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:chat` (\"Required scope: write:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to record feedback\", when saving the feedback raises.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/prompt_suggestions": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get prompt suggestions for the caller",
        "description": "Active, high-intelligence prompt templates matched to the caller's\npreferred complexity, best `intelligence_score` first — the system's\ntemplates and this business's own, never another business's. A lookup error\ninside the service answers an empty list, not a 500.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Only templates in this prompt category.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "user_intent",
            "in": "query",
            "description": "Only templates whose name or content contains this text (case-insensitive).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "max_suggestions",
            "in": "query",
            "description": "How many templates to return, at most 20. Missing, zero, negative or not a whole number reads as 5.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Prompt suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "suggestions",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IntelligencePromptSuggestion"
                      }
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate prompt suggestions\", with an empty `suggestions` array beside it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "suggestions": {
                          "type": "array",
                          "maxItems": 0,
                          "items": {}
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/workflow_defaults": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get recommended defaults for a workflow",
        "description": "Recommended steps, duration, complexity and assistant for a workflow\ntype, built from the business's successful workflow patterns of that\ntype (success rate 70+, rolling confidence 60+, top ten). With no such\npattern the fallback set is returned: no steps, 300 seconds,\n`moderate`, 0.5 success probability, `task_assistant`, confidence\n0.1.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_type",
            "in": "query",
            "description": "The interaction pattern type to recommend for (e.g. `task_creation`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_context",
            "in": "query",
            "description": "Accepted but not used.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Workflow defaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "workflow_defaults",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "workflow_defaults": {
                      "type": "object",
                      "required": [
                        "workflow_type",
                        "recommended_steps",
                        "estimated_duration",
                        "complexity_level",
                        "success_probability",
                        "suggested_ai_assistant",
                        "context_requirements",
                        "confidence_score"
                      ],
                      "properties": {
                        "workflow_type": {
                          "type": "string",
                          "nullable": true,
                          "description": "The `workflow_type` asked for."
                        },
                        "recommended_steps": {
                          "type": "array",
                          "items": {}
                        },
                        "estimated_duration": {
                          "type": "number",
                          "description": "Seconds; 300 when unknown."
                        },
                        "complexity_level": {
                          "type": "string",
                          "enum": [
                            "simple",
                            "moderate",
                            "complex"
                          ]
                        },
                        "success_probability": {
                          "type": "number",
                          "description": "0..1; 0.5 when unknown."
                        },
                        "suggested_ai_assistant": {
                          "type": "string",
                          "example": "task_assistant"
                        },
                        "context_requirements": {
                          "type": "array",
                          "description": "Up to five context keys the successful patterns most often carried.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "confidence_score": {
                          "type": "number"
                        }
                      }
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate workflow defaults\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/pattern_analytics": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get analytics of the caller's interaction patterns",
        "description": "Counts, success rates and confidence of the caller's own interaction\npatterns in this business, by pattern type, with a trend series and\ntheir ten most successful patterns.\n\n`time_range` limits the patterns to the last 7, 30 or 90 days by\ncreation; any other value applies no time limit. `trend_data`: for\n`7_days` and `30_days`, one key per day (`YYYY-MM-DD`) with that\nday's count (0 for a day with none); for `90_days`, the count per\nweek keyed by the week's start timestamp; otherwise the count per day\nkeyed by date.\n\n**Required Scopes:** `read:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:chat\") before\nanything else is checked. Its own_ form `read:own_chat`, `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "time_range",
            "in": "query",
            "description": "How far back to look. Any other value means no time limit.",
            "schema": {
              "type": "string",
              "enum": [
                "7_days",
                "30_days",
                "90_days"
              ],
              "default": "30_days"
            }
          },
          {
            "name": "pattern_types[]",
            "in": "query",
            "description": "Only these pattern types, sent as an array (`pattern_types[]=a&pattern_types[]=b`) or as one plain `pattern_types=a` value.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pattern analytics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "analytics",
                    "time_range",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "analytics": {
                      "type": "object",
                      "required": [
                        "total_patterns",
                        "pattern_distribution",
                        "success_rates",
                        "confidence_scores",
                        "trend_data",
                        "top_patterns"
                      ],
                      "properties": {
                        "total_patterns": {
                          "type": "integer"
                        },
                        "pattern_distribution": {
                          "type": "object",
                          "description": "Pattern type → count.",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "success_rates": {
                          "type": "object",
                          "description": "Pattern type → average success rate (decimal, as a string).",
                          "additionalProperties": {
                            "type": "string"
                          }
                        },
                        "confidence_scores": {
                          "type": "object",
                          "description": "Pattern type → average confidence (decimal, as a string).",
                          "additionalProperties": {
                            "type": "string"
                          }
                        },
                        "trend_data": {
                          "type": "object",
                          "description": "Date or week → count (see the operation description).",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "top_patterns": {
                          "type": "array",
                          "description": "Up to ten patterns, highest success rate first.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "pattern_type": {
                                "type": "string"
                              },
                              "success_rate": {
                                "type": "string",
                                "description": "Decimal, as a string."
                              },
                              "confidence_score": {
                                "type": "string",
                                "description": "Decimal, as a string."
                              },
                              "occurrence_count": {
                                "type": "integer"
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    },
                    "time_range": {
                      "type": "string",
                      "description": "The `time_range` asked for (default `30_days`)."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:chat` (\"Required scope: read:chat\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate pattern analytics\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/performance_metrics": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Get AI-assistant intelligence performance metrics (admins)",
        "description": "Health and performance of the AI-assistant intelligence system for an\nadmin: the answering server process's own operation timings over the\nlast hour (in-memory, so they differ between servers and reset on\nrestart), this business's profile, pattern and session counts, a\nsystem health check, and a 24-hour performance report for the\nbusiness.\n\nOnly an admin or super admin of the business is answered; anyone else\ngets `403 forbidden` (\"Unauthorized\").\n\n**Required Scopes:** `read:analytics`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:analytics\")\nbefore anything else is checked. Only the sign-in doors mint it, so a\ntoken made by hand never carries an admin's width; `admin` and\n`read:all` also pass. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Performance metrics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "metrics",
                    "generated_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "metrics": {
                      "type": "object",
                      "required": [
                        "system_performance",
                        "business_performance",
                        "system_health",
                        "performance_report"
                      ],
                      "properties": {
                        "system_performance": {
                          "type": "object",
                          "description": "This server process's last hour.",
                          "properties": {
                            "total_operations": {
                              "type": "integer"
                            },
                            "success_rate": {
                              "type": "number"
                            },
                            "avg_response_time": {
                              "type": "number"
                            },
                            "p95_response_time": {
                              "type": "number"
                            },
                            "error_rate": {
                              "type": "number"
                            },
                            "cache_performance": {
                              "type": "object",
                              "properties": {
                                "hit_rate": {
                                  "type": "number"
                                },
                                "total_requests": {
                                  "type": "integer"
                                }
                              }
                            },
                            "slow_operations": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "error_breakdown": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "system_health": {
                              "type": "string"
                            }
                          }
                        },
                        "business_performance": {
                          "$ref": "#/components/schemas/IntelligenceBusinessPerformance"
                        },
                        "system_health": {
                          "type": "object",
                          "properties": {
                            "overall_status": {
                              "type": "string",
                              "example": "healthy"
                            },
                            "checks": {
                              "type": "object",
                              "description": "One object each for `database`, `materialized_views`, `background_jobs`, `cache_performance`, `api_performance` and `error_rates`.",
                              "additionalProperties": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            },
                            "alerts": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string"
                                  },
                                  "component": {
                                    "type": "string"
                                  },
                                  "message": {
                                    "type": "string"
                                  },
                                  "severity": {
                                    "type": "string"
                                  }
                                }
                              }
                            },
                            "last_check": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        },
                        "performance_report": {
                          "type": "object",
                          "properties": {
                            "generated_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "time_range": {
                              "description": "The report window (24 hours), as Rails serializes a duration."
                            },
                            "report_type": {
                              "type": "string",
                              "enum": [
                                "business"
                              ]
                            },
                            "business_metrics": {
                              "$ref": "#/components/schemas/IntelligenceBusinessPerformance"
                            },
                            "recommendations": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            },
                            "action_items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:analytics` (\"Required scope: read:analytics\").\n- `forbidden` — \"Unauthorized\": the caller is not an admin or super admin of the business.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to generate performance metrics\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intelligence/refresh_patterns": {
      "post": {
        "tags": [
          "Intelligence"
        ],
        "summary": "Schedule a re-analysis of a person's interaction patterns",
        "description": "Queues a pattern analysis (UserPatternAnalysisJob) for the caller, or\n— for an admin or super admin of the business — for the person named\nby `user_id`. Creates that person's intelligence profile for this\nbusiness if they have none.\n\n`user_id` must be a member of this business (any membership row) or\nthe call answers `404 not_found`. A caller who is not an admin or\nsuper admin may only name themselves; anyone else gets\n`403 forbidden` (\"Unauthorized\").\n\n**Required Scopes:** `write:chat`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:chat\") before\nanything else is checked. For your own patterns (no `user_id`, or\nyour own id) its own_ form `write:own_chat` and `admin` also pass.\nRefreshing ANOTHER person's patterns (a `user_id` that is not the\ncaller's) asks the wide `write:chat` itself — `write:own_chat` does\nnot stand in for it; only `write:chat` or `admin` pass. A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "Whose patterns to refresh; the caller when omitted. Also read from the query string."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Analysis queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "user_id",
                    "scheduled_at"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Pattern analysis scheduled"
                    },
                    "user_id": {
                      "type": "integer"
                    },
                    "scheduled_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/IntelligenceBusinessContextRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:chat` (\"Required scope: write:chat\"); for another person's `user_id`, the token holds only the own_ form.\n- `forbidden` — \"Unauthorized\": a caller who is not an admin or super admin named someone else.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — \"The requested resource was not found.\": `user_id` is not a member of this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_parameter` — \"user_id must be one person's id\": `user_id` was sent as a list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_server_error` \"Failed to schedule pattern refresh\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employees/{id}": {
      "get": {
        "tags": [
          "Employees"
        ],
        "summary": "Get a colleague's directory card",
        "description": "One person's directory card in the caller's business. `{id}` is the\nperson's user id and is looked up among everyone with a membership in\nthis business — a deactivated membership is still found; anyone else\nis `404 not_found`.\n\n`email` and `phone` are included only for a browser session or a\ntoken holding `read:users` (`admin` and `read:all` count); every\nother token gets the card without them.\n\nEvery card field also follows the People directory's field rules\nfor the caller (a token's own user): `position`, `manager` and\n`department` are null, and `email` and `phone` are left out, where\nthe directory hides that field from them. An admin, or a business\nwithout People field rules, gets every field.\n\n**Required Scopes:** `read:people_directory`. A token without it gets\n`403 insufficient_permissions` (\"Insufficient permissions. Required\nscope: read:people_directory\") before anything else is checked.\n`read:users` implies it, and `admin` and `read:all` also pass. A\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The person's user id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The directory card",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeesCard"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:people_directory` (\"Insufficient permissions. Required scope: read:people_directory\"); `code` is repeated at the top level.\n- `forbidden` — \"No business is selected for this session.\" (browser session only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeesScopeError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — \"Employee not found\": no member of this business has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employees/{id}/schedule": {
      "get": {
        "tags": [
          "Employees"
        ],
        "summary": "Get a person's shift schedule",
        "description": "The shifts `{id}` is assigned to in this business whose start falls\nbetween `start_date` 00:00 and `end_date` 23:59:59 in the caller's\ntime zone, earliest first. Every assigned shift is listed, whatever\nits status (cancelled included). Each shift's date and times are\ngiven in the shift's own location time zone.\n\nWithout dates the window is today through seven days later; without\n`end_date` it is `start_date` plus seven days. The window is not\ncapped, and an `end_date` before `start_date` returns no shifts.\n\nA caller reads their own schedule; another person's needs the caller\nto be a manager, admin or super admin of the business\n(`403 forbidden` otherwise).\n\n**Required Scopes:** `read:shifts`. A token without it gets\n`403 insufficient_permissions` (\"Insufficient permissions. Required\nscope: read:shifts\") before anything else is checked. For the\ncaller's own id the own_ form `read:own_shifts` also passes; for\nanyone else's only the wide `read:shifts` does. `admin` and\n`read:all` pass either way. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The person's user id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "First day of the window, `YYYY-MM-DD` (any format Ruby's `Date.parse` reads is accepted). Defaults to today.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Last day of the window, `YYYY-MM-DD`. Defaults to `start_date` plus seven days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The schedule",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "employee_id",
                    "employee_name",
                    "start_date",
                    "end_date",
                    "shifts"
                  ],
                  "properties": {
                    "employee_id": {
                      "type": "integer"
                    },
                    "employee_name": {
                      "type": "string"
                    },
                    "start_date": {
                      "type": "string",
                      "format": "date"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date"
                    },
                    "shifts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EmployeesScheduleShift"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — \"Invalid date format. Please use YYYY-MM-DD.\": `start_date` or `end_date` cannot be parsed as a date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:shifts` (\"Insufficient permissions. Required scope: read:shifts\"), or holds only `read:own_shifts` for someone else's id; `code` is repeated at the top level.\n- `forbidden` — \"You can read your own schedule; a colleague's is for their manager.\": the caller is not a manager, admin or super admin of the business and `{id}` is not their own; `code` is repeated at the top level.\n- `forbidden` — \"No business is selected for this session.\" (browser session only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeesScopeError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — \"Employee not found\": no member of this business has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recent_entities": {
      "get": {
        "tags": [
          "Recent Entities"
        ],
        "summary": "List recently updated employees, shifts or locations",
        "description": "Up to `limit` records of one type from the caller's business, most\nrecently updated first.\n\n- `employee` — everyone with a membership in the business\n  (deactivated memberships included), by the user record's\n  `updated_at`. A row carries the person's `email` only for a browser\n  session or a token holding `read:users` (`admin` and `read:all`\n  count), and only where the People directory shows email to the\n  caller — the rule a colleague's card (GET /employees/{id}) applies;\n  everyone else gets the row without it. `position` is null where the\n  directory hides Job Function from the caller.\n- `shift` — for a manager, admin or super admin of the business, all\n  its shifts; for anyone else, only the shifts they are assigned to.\n- `location` — all the business's locations, active or not.\n\n`updated_at` is `YYYY-MM-DD HH:MM` in the caller's time zone (no\noffset); a shift's `date` and `time` are in the shift location's\ntime zone.\n\n**Required Scopes:** depends on `entity_type`:\n\n| `entity_type` | Scope asked |\n|---|---|\n| `employee` | `read:people_directory` (`read:users` implies it) |\n| `shift` | `read:shifts` — the wide scope; `read:own_shifts` does not pass, even though a member is shown only their own shifts |\n| `location` | `read:organization` |\n\nA token without the type's scope gets `403 insufficient_permissions`\n(\"Insufficient permissions. Required scope: …\"). `admin` and\n`read:all` pass for every type. A blank `entity_type` is refused with\n400 before any scope is asked, and an unknown type asks no scope and\nis refused with 400. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entity_type",
            "in": "query",
            "required": true,
            "description": "Which kind of record to list.",
            "schema": {
              "type": "string",
              "enum": [
                "employee",
                "shift",
                "location"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many records to return, capped at 50. A missing, zero, negative or non-numeric value reads as the default, 10.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The recent records of the requested type",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RecentEntitiesEmployeeList"
                    },
                    {
                      "$ref": "#/components/schemas/RecentEntitiesShiftList"
                    },
                    {
                      "$ref": "#/components/schemas/RecentEntitiesLocationList"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "entity_type",
                    "mapping": {
                      "employee": "#/components/schemas/RecentEntitiesEmployeeList",
                      "shift": "#/components/schemas/RecentEntitiesShiftList",
                      "location": "#/components/schemas/RecentEntitiesLocationList"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_request` — \"Entity type is required\": `entity_type` is missing or blank.\n- `invalid_request` — \"Unknown entity type: <value>\": `entity_type` is not `employee`, `shift` or `location`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks the type's scope (\"Insufficient permissions. Required scope: read:people_directory\" / \"read:shifts\" / \"read:organization\"); `code` is repeated at the top level.\n- `forbidden` — \"No business is selected for this session.\" (browser session only).\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "insufficient_permissions"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/notes": {
      "post": {
        "tags": [
          "Notes"
        ],
        "summary": "Add a Performance note to a record",
        "description": "Creates a note authored by the caller on one record of their business.\nThe record is named by `notable_type` + `notable_id` (or the aliases\n`entity_type` + `entity_id`; `notable_*` wins when both are sent).\n\nWho may add a note, per type:\n\n| `notable_type` | The caller may add a note when |\n|---|---|\n| `PerformanceReview` | they may access the review |\n| `Goal` | it is their own goal, or they may manage its employee |\n| `Apps::Epms::Meeting` | they may open the meeting |\n| `DevelopmentPlan` | it is their own plan, or they may manage its employee |\n| `User` | it is themselves, or they may manage that person |\n\nA type outside this list, a missing id, a record that is not this\nbusiness's and a record the caller may not see all get the same\n`403` (\"You do not have access to that record.\") — it never says\nwhether the record exists.\n\nThe Performance app's **Notes & Attachments** module must be enabled\nfor the business; otherwise `403` (\"Notes & Attachments is not\nenabled for this business.\").\n\nHonours `Idempotency-Key`: a retry with the same key returns the\nstored `201` instead of creating a second note.\n\n**Required Scopes:** the write scope of the record the note hangs on:\n\n| `notable_type` | Scope asked |\n|---|---|\n| `PerformanceReview` | `write:epms_reviews` |\n| `Goal` | `write:epms_goals` |\n| `Apps::Epms::Meeting` | `write:epms_meetings` |\n| `DevelopmentPlan` | `write:epms_development` |\n| `User` | `write:epms_feedback` |\n\nA token without it gets `403 insufficient_permissions`\n(\"Insufficient permissions. Required scope: write:epms_goals\") before\nthe module or the record is checked; `admin` also passes. A type\noutside the table asks no scope and is refused by the access check.\nA browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotesCreateRequest"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/NotesCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The note was created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "note_id",
                    "message"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "note_id": {
                      "type": "integer"
                    },
                    "message": {
                      "type": "string",
                      "example": "Note created successfully"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "- `unauthorized` — \"Unauthorized access\": no valid token or session.\n- `unauthorized` — \"Current user not available\": the request authenticated without a person.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks the record type's write scope (\"Insufficient permissions. Required scope: write:epms_reviews\" and so on); `code` is repeated at the top level. Error envelope.\n- `forbidden` — \"No business is selected for this session.\" (browser session only). Error envelope.\n- `{ success: false, message: \"Notes & Attachments is not enabled for this business.\" }` — the Performance Notes & Attachments module is off.\n- `{ success: false, message: \"You do not have access to that record.\" }` — the type is not one of the five, the id is missing, the record is not this business's, or the caller may not see it.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Error"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "example": "insufficient_permissions"
                            }
                          }
                        }
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/NotesRefusal"
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "- `{ success: false, message: \"Invalid note type.\" }` — `note_type` is not one of the listed values.\n- `{ success: false, message: \"Invalid visibility.\" }` — `visibility` is not one of the listed values.\n- `{ success: false, message: \"Failed to create note\", errors: [...] }` — the note did not save, e.g. \"Content can't be blank\" or \"Content is too long (maximum is 5000 characters)\".\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/NotesRefusal"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "array",
                          "description": "The model's full error messages; only on a failed save.",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/integrations/social": {
      "get": {
        "tags": [
          "Social Publishing"
        ],
        "summary": "List connected social providers",
        "description": "Returns the list of social-publishing providers the calling business has\nconnected and what each one can do. External CMS integrations call this\nfirst to feature-detect available providers before constructing a publish.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Connected providers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "provider",
                          "status",
                          "capabilities"
                        ],
                        "properties": {
                          "provider": {
                            "type": "string",
                            "example": "linkedin",
                            "description": "Provider slug under the 'social' category."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "enabled",
                              "disabled"
                            ]
                          },
                          "health": {
                            "type": "string",
                            "enum": [
                              "healthy",
                              "warning",
                              "error",
                              "unknown"
                            ]
                          },
                          "connected_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "capabilities": {
                            "type": "array",
                            "description": "Flat list of capability strings. LinkedIn capabilities:\n`publish_organization` (org_urn configured),\n`publish_member` (linkedin_member_id configured).\n",
                            "items": {
                              "type": "string",
                              "example": "publish_organization"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token missing `read:social_publishing` scope."
          }
        }
      }
    },
    "/integrations/social/linkedin/posts": {
      "post": {
        "tags": [
          "Social Publishing"
        ],
        "summary": "Publish a post to LinkedIn",
        "description": "Publishes a post to the connected LinkedIn organization page (default)\nor to the connected member's personal feed. The business must have a\nLinkedIn `SystemIntegration` row in `enabled` status.\n\n**Idempotency:** pass an `Idempotency-Key` header to dedupe retries —\nthe response is cached for 24h via Redis, and the\n`social_publish_logs` table has a unique constraint on\n`(business_id, idempotency_key)` as a fallback.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "schema": {
              "type": "string"
            },
            "description": "Optional UUID for retry-safe dedup."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "Post body. Truncated to 3000 characters.",
                    "example": "We're hiring for our retail-ops team — come build with us."
                  },
                  "author": {
                    "type": "string",
                    "enum": [
                      "organization",
                      "member"
                    ],
                    "default": "organization"
                  },
                  "link_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true,
                    "description": "Optional article URL — attaches a link card."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "PUBLIC",
                      "CONNECTIONS"
                    ],
                    "default": "PUBLIC",
                    "description": "`CONNECTIONS` only applies to `author: member`. LinkedIn\nrejects it for organization posts.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post published or scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialPostEnvelope"
                }
              }
            }
          },
          "412": {
            "description": "LinkedIn is not connected for this business. Admin must complete\nOAuth at `/admin/integrations/browse_integrations?category=social`\nbefore publishing.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "Invalid payload (missing text, bad author value).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "502": {
            "description": "Upstream LinkedIn error. `error.code` is one of:\n`rate_limited`, `provider_error`, `auth_expired`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token missing `write:social_publishing` scope."
          },
          "409": {
            "description": "Idempotency-Key reused with a different request body."
          }
        }
      }
    },
    "/integrations/social/linkedin/posts/{urn}": {
      "parameters": [
        {
          "in": "path",
          "name": "urn",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "LinkedIn URN of a previously-published post (e.g.\n`urn:li:share:1234567890`). URL-encoded colons are accepted.\n"
        }
      ],
      "get": {
        "tags": [
          "Social Publishing"
        ],
        "summary": "Fetch publish status by URN",
        "description": "Reads the `social_publish_logs` row for a previously-published URN.\nDoes NOT round-trip to LinkedIn — status reflects what we recorded\nat publish time.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Publish log entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialPostEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No log row matches that URN for the calling business."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token missing `read:social_publishing` scope."
          }
        }
      },
      "delete": {
        "tags": [
          "Social Publishing"
        ],
        "summary": "Delete a published LinkedIn post",
        "description": "Calls LinkedIn's delete endpoint and marks the log row deleted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "412": {
            "description": "LinkedIn is not connected."
          },
          "502": {
            "description": "Upstream LinkedIn error."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token missing `write:social_publishing` scope."
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List active webhooks",
        "description": "The business's ACTIVE webhooks (a webhook switched off, by hand or\nafter repeated failures, is left out — read it with GET\n/webhooks/{id}), paginated, in no guaranteed order. No webhook in the\nlist carries its signing secret.\n\n**Required Scopes:** `read:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:webhooks\")\nbefore anything else is checked. The caller must also be an admin or\nsuper admin of the business, whatever the token holds. A browser\nsession is asked no scope.\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": "Webhooks per page. Clamped to 1..100; a blank or non-numeric value reads as 0 and is clamped to 1.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the business's active webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhooks",
                    "meta"
                  ],
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhooksWebhook"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WebhooksListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:webhooks` (\"Required scope: read:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook",
        "description": "Registers a webhook for the caller's business. The response is the only\none that carries the webhook's signing secret (`secret_key`): one is\ngenerated (40 hex characters) unless the body sets its own. Keep it —\nno later response returns it.\n\n`url` must be a full `http://` or `https://` address with a host, no\n`user:password@` and no `#fragment`, and unique within the business.\n`event_types` must be non-empty and name events from GET\n/webhooks/event_types (its `event_types`, or — for a caller who may\nmanage webhooks — a `plugin_event_types[].event_type`), or `\"*\"` for\nevery event in `event_types` (never a plugin event, and never the\npeople lifecycle `person.*` events, which a webhook receives only by\nnaming them). Every name the platform knows is accepted, including\nevents whose platform switch is off.\n\nA webhook has no name field: `description` is its label. A `name` key\nis accepted as an alias for `description` when `description` is not\nsent, and ignored when it is.\n\n**Required Scopes:** `write:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:webhooks\")\nbefore anything else is checked. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhook"
                ],
                "properties": {
                  "webhook": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/WebhooksWebhookInput"
                      },
                      {
                        "type": "object",
                        "required": [
                          "url",
                          "event_types"
                        ]
                      }
                    ]
                  }
                }
              },
              "example": {
                "webhook": {
                  "url": "https://hooks.example.com/workforce",
                  "description": "Payroll sync",
                  "event_types": [
                    "leave_request.approved",
                    "shift.created"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The webhook, with its signing secret (this response only)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhook"
                  ],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/WebhooksWebhookWithSecret"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `webhook` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:webhooks` (\"Required scope: write:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The webhook was not saved. `errors` holds one sentence per failed\nvalidation, for example \"URL must be a full http:// or https://\naddress, such as https://example.com/webhook\", \"URL must not\ninclude a username or password — receivers verify the\nX-Webhook-Signature header instead\", \"URL must not include a\n#fragment (anything after #)\", \"URL has already been taken\",\n\"Event types can't be blank\" or \"Event types contains invalid\nevent types: …\".\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhooksValidationMessages"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/event_types": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List the event types a webhook can subscribe to",
        "description": "`event_types` is the platform catalogue a webhook can subscribe to right\nnow — exactly what `\"*\"` covers apart from the `person.*` lifecycle\nevents — less the events waiting on a platform switch that is off (a\nsubscription to one would hear nothing). The list is the same for every\nbusiness.\n\n`plugin_event_types` lists the webhook events (declared with\n`outgoing_event`) of the plugins that can run for this business, each\nsubscribable only by naming it. It is returned only to a caller who is\nan admin or super admin of the business; anyone else gets\n`event_types` alone (the key is absent, not empty).\n\n**Required Scopes:** `read:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:webhooks\").\nThis is the one action with no admin gate. A browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The subscribable event types",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event_types"
                  ],
                  "properties": {
                    "event_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "shift.created",
                        "shift_assignment.updated",
                        "leave_request.approved",
                        "attendance.missed"
                      ]
                    },
                    "plugin_event_types": {
                      "type": "array",
                      "description": "Present only for an admin or super admin caller.",
                      "items": {
                        "type": "object",
                        "required": [
                          "event_type",
                          "plugin_slug",
                          "plugin_name"
                        ],
                        "properties": {
                          "event_type": {
                            "type": "string",
                            "description": "\"plugin.<slug>.<event>\" — the name to put in a webhook's `event_types`.",
                            "example": "plugin.expense_tracker.expense.approved"
                          },
                          "plugin_slug": {
                            "type": "string",
                            "example": "expense_tracker"
                          },
                          "plugin_name": {
                            "type": "string",
                            "description": "The plugin's name, else its slug.",
                            "example": "Expense Tracker"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:webhooks` (\"Required scope: read:webhooks\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/verify": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Check a URL before registering it as a webhook",
        "description": "Sends one signed test event (`event_type` \"verification\", signed with a\nthrowaway secret) to `url` and reports whether the receiver answered\n2xx. Nothing is saved: no webhook, no delivery record. The request\nleaves our servers under the same guard real deliveries use (http or\nhttps only, a public host, 5 s to connect, 10 s to answer).\n\n**Required Scopes:** `write:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:webhooks\")\nbefore anything is sent. The caller must also be an admin or super\nadmin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The address to test. Top-level, not wrapped in `webhook`.",
                    "example": "https://hooks.example.com/workforce"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The receiver accepted the event",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "url",
                    "test_result"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "URL verification successful"
                    },
                    "url": {
                      "type": "string"
                    },
                    "test_result": {
                      "$ref": "#/components/schemas/WebhooksVerifyResult"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:webhooks` (\"Required scope: write:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Either `url` was blank — `{\"error\": {\"code\": \"validation_failed\",\n\"message\": \"URL is required\"}}` and nothing is sent — or the\nverification failed. A failure carries `error.code`\n`validation_failed` with the reason as `error.message` (for\nexample \"HTTP 500\", or \"URL hostname did not resolve to a public\naddress\" when the guard refused the host), beside `message`\n(\"URL verification failed\"), `url` and `test_result`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "message": {
                          "type": "string",
                          "example": "URL verification failed"
                        },
                        "url": {
                          "type": "string"
                        },
                        "test_result": {
                          "$ref": "#/components/schemas/WebhooksVerifyResult"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Webhook ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook",
        "description": "One webhook of the caller's business, active or not, without its\nsigning secret.\n\n**Required Scopes:** `read:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:webhooks\")\nbefore the webhook is looked up. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The webhook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhook"
                  ],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/WebhooksWebhook"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:webhooks` (\"Required scope: read:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no webhook with that id in the caller's business (\"Webhook not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook",
        "description": "Changes any of the fields in `webhook`; the rest are left alone. The\nresponse never carries the signing secret, even one the body has just\nset. A `name` key is an alias for `description` when `description` is\nnot sent. Event names already on the webhook stay valid even if they\ncould no longer be added (a disabled plugin's event, an event whose\nplatform switch is now off); only names being added are checked.\n\n**Required Scopes:** `write:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:webhooks\")\nbefore the webhook is looked up. Switching a live webhook off —\n`active` false, `\"0\"`, `\"\"` or null, anything that does not read as\ntrue, sent to a webhook that is on — stops every delivery to it, so it\nalso asks `destructive:webhooks` on top of `write:webhooks`.\nRe-sending an off value to a webhook already off, switching one on,\nand every other edit need `write:webhooks` alone. The caller must also\nbe an admin or super admin of the business. A browser session is\nasked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhook"
                ],
                "properties": {
                  "webhook": {
                    "$ref": "#/components/schemas/WebhooksWebhookInput"
                  }
                }
              },
              "example": {
                "webhook": {
                  "description": "Payroll sync (EU)",
                  "event_types": [
                    "leave_request.approved"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated webhook (without its signing secret)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhook"
                  ],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/WebhooksWebhook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `webhook` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:webhooks` (\"Required scope: write:webhooks\"), or the update switches a live webhook off and the token lacks `destructive:webhooks` (\"Required scope: destructive:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no webhook with that id in the caller's business (\"Webhook not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The change was not saved; `errors` holds one sentence per failed\nvalidation (the same messages as POST /webhooks).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhooksValidationMessages"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook (PUT)",
        "description": "Same as PATCH /webhooks/{id}: the same body, gates and responses,\nincluding `destructive:webhooks` on top of `write:webhooks` for an\nupdate that switches a live webhook off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhook"
                ],
                "properties": {
                  "webhook": {
                    "$ref": "#/components/schemas/WebhooksWebhookInput"
                  }
                }
              },
              "example": {
                "webhook": {
                  "description": "Payroll sync (EU)",
                  "event_types": [
                    "leave_request.approved"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated webhook (without its signing secret)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhook"
                  ],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/WebhooksWebhook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `webhook` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:webhooks` (\"Required scope: write:webhooks\"), or the update switches a live webhook off and the token lacks `destructive:webhooks` (\"Required scope: destructive:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no webhook with that id in the caller's business (\"Webhook not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The change was not saved; `errors` holds one sentence per failed\nvalidation (the same messages as POST /webhooks).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhooksValidationMessages"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook",
        "description": "Deletes the webhook and its delivery history. Deliveries stop at once.\n\n**Required Scopes:** `destructive:webhooks` — the area's delete level,\nasked instead of `write:webhooks` (a token holding only\n`write:webhooks` is refused). A token without it gets\n`403 insufficient_permissions` (\"Required scope: destructive:webhooks\")\nbefore the webhook is looked up. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted; no body"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:webhooks` (\"Required scope: destructive:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no webhook with that id in the caller's business (\"Webhook not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test event to a webhook",
        "description": "Delivers one signed `test.event` to the webhook's URL now — one\nattempt, no retries — whether the webhook is active or not, and\nanswers with the delivery record. The delivery is kept in the\nwebhook's delivery log. A success records `last_triggered_at` and\nresets `consecutive_failures`. A failed test is answered `422` and\nnever counted toward `consecutive_failures` — only real deliveries\ncan switch a webhook off. No body is needed.\n\n**Required Scopes:** `write:webhooks`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:webhooks\")\nbefore anything is sent. The caller must also be an admin or super\nadmin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The receiver answered 2xx",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "test_result"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Webhook test successful"
                    },
                    "test_result": {
                      "$ref": "#/components/schemas/WebhooksTestResult"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:webhooks` (\"Required scope: write:webhooks\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no webhook with that id in the caller's business (\"Webhook not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The test delivery failed. Not the error envelope: the body is\n`message` (\"Webhook test failed\") and `test_result`, whose `error`\nsays why (\"HTTP 500\", a network error, or the URL guard's reason).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "test_result"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Webhook test failed"
                    },
                    "test_result": {
                      "$ref": "#/components/schemas/WebhooksTestResult"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/forecasts": {
      "get": {
        "tags": [
          "Forecasts"
        ],
        "summary": "List forecasts",
        "description": "The business's forecasts, active or not, latest `forecast_date` first,\npaginated, each with its full location row. Demand signals are not\nincluded here (see GET /forecasts/{id} or /forecasts/date_range).\n\nOptional filters: `location_id`, `source`, and a date window that\napplies only when BOTH `start_date` and `end_date` are sent\n(inclusive).\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\") before anything else is checked. Any\nmember of the business may read. A browser session is asked no scope.\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": "Forecasts per page. Clamped to 1..100; a blank or non-numeric value reads as 0 and is clamped to 1.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Only forecasts for this location.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Only forecasts with this `source` (see GET /forecasts/types).",
            "schema": {
              "type": "string",
              "example": "api"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "First day of the window (YYYY-MM-DD). Ignored unless `end_date` is sent too.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Last day of the window (YYYY-MM-DD). Ignored unless `start_date` is sent too.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of forecasts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "forecasts",
                    "meta"
                  ],
                  "properties": {
                    "forecasts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ForecastsForecastWithLocation"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ForecastsListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Create a forecast",
        "description": "Creates the forecast for one location on one date. A second forecast\nfor the same location and date is refused (422 \"Forecast date has\nalready been taken\") — update the existing one instead.\n\n`demand_signals`, when sent, are created at the new forecast's location\nand date (an `id` in them is ignored). A signal the model refuses does\nnot undo the forecast: the response is still 201, with one message per\nrefused signal in `demand_signal_errors`.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore anything else is checked. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "forecast"
                ],
                "properties": {
                  "forecast": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ForecastsForecastInput"
                      },
                      {
                        "type": "object",
                        "required": [
                          "location_id",
                          "forecast_date"
                        ]
                      }
                    ]
                  },
                  "demand_signals": {
                    "type": "array",
                    "description": "Signals to create at this forecast's location and date.",
                    "items": {
                      "$ref": "#/components/schemas/ForecastsNestedDemandSignalInput"
                    }
                  }
                }
              },
              "example": {
                "forecast": {
                  "location_id": 12,
                  "forecast_date": "2026-10-15",
                  "confidence": 80,
                  "hourly_predictions": [
                    {
                      "hour": 9,
                      "predicted_volume": 40,
                      "min_staff_needed": 2,
                      "optimal_staff_needed": 3,
                      "max_staff_needed": 4
                    },
                    {
                      "hour": 10,
                      "predicted_volume": 65,
                      "min_staff_needed": 3,
                      "optimal_staff_needed": 4,
                      "max_staff_needed": 5
                    }
                  ]
                },
                "demand_signals": [
                  {
                    "signal_type": "event",
                    "name": "Stadium concert",
                    "impact_factor": 1.35
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The forecast, with its location and the live demand signals at its location and date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsForecastResponse"
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `forecast` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — `location_id` is not one of the business's locations (\"Location not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Not saved. `errors` holds one sentence per failed validation —\n\"Location can't be blank\" (no `location_id`), \"Forecast date can't\nbe blank\", \"Forecast date has already been taken\", \"Confidence must\nbe less than or equal to 100\", \"Hourly predictions hour must be\nbetween 0 and 23\", and so on.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsValidationMessages"
                }
              }
            }
          }
        }
      }
    },
    "/forecasts/date_range": {
      "get": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Forecasts for one location over a date range",
        "description": "Every forecast (active or not) for one location from `start_date` to\n`end_date` inclusive, earliest first, not paginated. Each carries the\nlive demand signals at that location on its date (highest\n`impact_factor` first); none carries its location row.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\"). Any member of the business may read.\nA browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": true,
            "description": "A location of the caller's business (another business's id simply matches nothing).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The forecasts in the range",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "forecasts",
                    "meta"
                  ],
                  "properties": {
                    "forecasts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ForecastsForecastWithSignals"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "location_id",
                        "start_date",
                        "end_date",
                        "total_count"
                      ],
                      "properties": {
                        "location_id": {
                          "type": "string",
                          "description": "The `location_id` query value, echoed as sent (a string).",
                          "example": "12"
                        },
                        "start_date": {
                          "type": "string",
                          "description": "Echoed as sent.",
                          "example": "2026-10-01"
                        },
                        "end_date": {
                          "type": "string",
                          "description": "Echoed as sent.",
                          "example": "2026-10-31"
                        },
                        "total_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` — \"location_id, start_date, and end_date are required\" (one of them is blank).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/forecasts/types": {
      "get": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Forecast sources",
        "description": "The values a forecast's `source` carries — `api` (written through this\nAPI) and `derived` (the autonomous baseline sweep). There is no\nforecast type: every forecast is a per-day staffing forecast, and\n`source` is the dimension GET /forecasts filters on. `source` is not\nvalidated against this list on write.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\"). A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The sources",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "forecast_sources"
                  ],
                  "properties": {
                    "forecast_sources": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "api",
                        "derived"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/forecasts/bulk_create": {
      "post": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Create many forecasts in the background",
        "description": "Queues a background job that creates one forecast per item and answers\n202 at once; poll `status_url` (GET /bulk_operations/status/{job_id})\nfor progress and per-item errors. Items are checked by the job, not\nhere: an item needs `location_id` (one of the business's locations)\nand `forecast_date`.\n\n`options.conflict_resolution` decides what an item does when a\nforecast already exists for its location and date: `skip` (default —\ncounted as processed, not successful), `overwrite` (the item's fields\nare written onto it), or `fail` (the item fails).\n`options.validation_mode`: `strict` (default) stops the whole\noperation at the first failed item — items already written stay\nwritten — and `lenient` records the failure and carries on.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore anything else is checked. A batch with\n`options.conflict_resolution` `overwrite` in which any item carries an\n`active` that does not read as true (false, `\"0\"`, `\"\"`, null)\nswitches existing forecasts off, so it also asks\n`destructive:scheduling` on top of `write:shifts`, and nothing is\nqueued without it. The caller must also be an admin or super admin of\nthe business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "forecasts"
                ],
                "properties": {
                  "forecasts": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/ForecastsBulkItem"
                    }
                  },
                  "options": {
                    "$ref": "#/components/schemas/ForecastsBulkOptions"
                  }
                }
              },
              "example": {
                "forecasts": [
                  {
                    "location_id": 12,
                    "forecast_date": "2026-10-15",
                    "confidence": 70
                  },
                  {
                    "location_id": 12,
                    "forecast_date": "2026-10-16",
                    "confidence": 65
                  }
                ],
                "options": {
                  "conflict_resolution": "overwrite",
                  "validation_mode": "lenient"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsBulkAccepted"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the batch overwrites forecasts with `active` off and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was queued.\n- `forecasts` was missing or empty: `{\"error\": {\"code\": \"validation_failed\", \"message\": \"No forecasts provided\"}}`.\n- The bulk operation could not be saved: `{\"errors\": [\"…\"]}`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ForecastsValidationMessages"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "`service_unavailable` — the job could not be queued (\"Could not\nschedule the bulk operation. Please try again.\"). The operation is\nsaved as `failed` and returned beside the error as\n`bulk_operation`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "bulk_operation": {
                          "$ref": "#/components/schemas/ForecastsBulkOperation"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/forecasts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Forecast ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Get a forecast",
        "description": "One forecast of the caller's business (active or not), with its\nlocation row and the live demand signals at its location and date,\nhighest `impact_factor` first.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\") before the forecast is looked up. Any\nmember of the business may read. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The forecast",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "forecast"
                  ],
                  "properties": {
                    "forecast": {
                      "$ref": "#/components/schemas/ForecastsForecastDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no forecast with that id in the caller's business (\"Forecast not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Update a forecast",
        "description": "Changes any of the fields in `forecast`; the rest are left alone.\n\nSend `demand_signals` to edit the signals at the forecast's location\nand date (as they stand after this save): an element with `id` updates\nthat signal — only one of this forecast's live signals; any other id is\nreported as \"signal N: not found for this forecast\" — and one without\n`id` creates a signal there. With `signals_strategy: \"replace\"`, every\nlive signal at that location and date is deleted first and the sent\nones are created. Signal writes the model refuses do not undo the\nforecast's save: the response is still 200, with one message per\nrefused signal in `demand_signal_errors`.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore the forecast is looked up. An update that ends something also\nasks `destructive:scheduling` on top of `write:shifts`:\n- switching a live forecast off — `forecast.active` false, `\"0\"`, `\"\"`\n  or null, anything that does not read as true (an inactive forecast\n  drops out of scheduling);\n- switching a live demand signal off through `demand_signals` (an\n  element with `id` and an `active` that is present and does not read\n  as true);\n- `signals_strategy: \"replace\"` when there are live signals at the\n  location and date to delete.\nEvery other edit, switching a forecast or signal on, and re-sending\noff to one already off need `write:shifts` alone. The caller must also\nbe an admin or super admin of the business. A browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "forecast"
                ],
                "properties": {
                  "forecast": {
                    "$ref": "#/components/schemas/ForecastsForecastInput"
                  },
                  "demand_signals": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ForecastsNestedDemandSignalInput"
                    }
                  },
                  "signals_strategy": {
                    "type": "string",
                    "enum": [
                      "replace"
                    ],
                    "description": "\"replace\" deletes the live signals at the location and date before creating the sent ones. Any other value (or none) updates by `id` and creates the rest. Read only when `demand_signals` is sent."
                  }
                }
              },
              "example": {
                "forecast": {
                  "confidence": 75
                },
                "demand_signals": [
                  {
                    "id": 88,
                    "impact_factor": 1.2
                  },
                  {
                    "signal_type": "promotion",
                    "name": "Weekend sale",
                    "impact_factor": 1.15
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated forecast, with its location and the live demand signals at its location and date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsForecastResponse"
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `forecast` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the update ends something (see above) and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` — no forecast with that id in the caller's business (\"Forecast not found\").\n- `not_found` — the sent `location_id` is not one of the business's locations (\"Location not found\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The forecast was not saved; `errors` holds one sentence per failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsValidationMessages"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Update a forecast (PUT)",
        "description": "Same as PATCH /forecasts/{id}: the same body, gates and responses,\nincluding `destructive:scheduling` on top of `write:shifts` for an\nupdate that ends something.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "forecast"
                ],
                "properties": {
                  "forecast": {
                    "$ref": "#/components/schemas/ForecastsForecastInput"
                  },
                  "demand_signals": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ForecastsNestedDemandSignalInput"
                    }
                  },
                  "signals_strategy": {
                    "type": "string",
                    "enum": [
                      "replace"
                    ],
                    "description": "\"replace\" deletes the live signals at the location and date before creating the sent ones. Any other value (or none) updates by `id` and creates the rest. Read only when `demand_signals` is sent."
                  }
                }
              },
              "example": {
                "forecast": {
                  "confidence": 75
                },
                "demand_signals": [
                  {
                    "id": 88,
                    "impact_factor": 1.2
                  },
                  {
                    "signal_type": "promotion",
                    "name": "Weekend sale",
                    "impact_factor": 1.15
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated forecast, with its location and the live demand signals at its location and date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsForecastResponse"
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `forecast` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the update ends something (see above) and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` — no forecast with that id in the caller's business (\"Forecast not found\").\n- `not_found` — the sent `location_id` is not one of the business's locations (\"Location not found\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The forecast was not saved; `errors` holds one sentence per failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastsValidationMessages"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Forecasts"
        ],
        "summary": "Delete a forecast",
        "description": "Deletes the forecast. The demand signals at its location and date are\nnot deleted — they do not belong to it.\n\n**Required Scopes:** `destructive:scheduling` — the area's delete\nlevel, asked instead of `write:shifts` (a token holding only\n`write:shifts` is refused). A token without it (or `admin`) gets\n`403 insufficient_permissions` (\"Required scope:\ndestructive:scheduling\") before the forecast is looked up, so a\nmissing id is a 403, not a 404. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted; no body"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no forecast with that id in the caller's business (\"Forecast not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/demand_signals": {
      "get": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "List demand signals",
        "description": "The business's demand signals, active or not, latest `signal_date`\nfirst (then newest id first, so paging is stable), paginated, each\nwith its full location row.\n\nOptional filters: `forecast_id` (the signals at that forecast's\nlocation and date, active or not), `signal_type`, `location_id`, and a\ndate window that applies only when BOTH `start_date` and `end_date`\nare sent (inclusive). Filters combine.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\") before anything else is checked. Any\nmember of the business may read. A browser session is asked no scope.\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": "Signals per page. Clamped to 1..100; a blank or non-numeric value reads as 0 and is clamped to 1.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "forecast_id",
            "in": "query",
            "description": "Only the signals at this forecast's location and date. 404 when the forecast is not in the caller's business.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "signal_type",
            "in": "query",
            "description": "Only signals of this type (see GET /demand_signals/types).",
            "schema": {
              "type": "string",
              "enum": [
                "event",
                "weather",
                "holiday",
                "promotion",
                "sale",
                "seasonal",
                "external"
              ]
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "Only signals at this location.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "First day of the window (YYYY-MM-DD). Ignored unless `end_date` is sent too.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Last day of the window (YYYY-MM-DD). Ignored unless `start_date` is sent too.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of demand signals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "demand_signals",
                    "meta"
                  ],
                  "properties": {
                    "demand_signals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DemandSignalsSignalWithLocation"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/DemandSignalsListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — `forecast_id` names no forecast in the caller's business (\"Forecast not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Create a demand signal",
        "description": "Creates a signal for one location on one date. `location_id`,\n`signal_type` and `signal_date` are required, and `name` too unless\n`signal_type` is \"weather\". `impact_factor` defaults to 1.0 (no\neffect); `value` is accepted as its older name.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore anything else is checked. The caller must also be an admin or\nsuper admin of the business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "demand_signal"
                ],
                "properties": {
                  "demand_signal": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/DemandSignalsSignalInput"
                      },
                      {
                        "type": "object",
                        "required": [
                          "location_id",
                          "signal_type",
                          "signal_date"
                        ]
                      }
                    ]
                  }
                }
              },
              "example": {
                "demand_signal": {
                  "location_id": 12,
                  "signal_type": "event",
                  "signal_date": "2026-10-15",
                  "name": "Stadium concert",
                  "impact_factor": 1.35
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The signal, with its location row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "demand_signal"
                  ],
                  "properties": {
                    "demand_signal": {
                      "$ref": "#/components/schemas/DemandSignalsSignalWithLocation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `demand_signal` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — `location_id` is not one of the business's locations (\"Location not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Not saved. `errors` holds one sentence per failed validation —\n\"Location can't be blank\" (no `location_id`), \"Signal type is not\nincluded in the list\", \"Signal date can't be blank\", \"Name can't be\nblank\", \"Impact factor must be greater than 0\", and so on.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemandSignalsValidationMessages"
                }
              }
            }
          }
        }
      }
    },
    "/demand_signals/types": {
      "get": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Demand signal types",
        "description": "The values accepted for a signal's `signal_type`.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\"). A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The signal types",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signal_types"
                  ],
                  "properties": {
                    "signal_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "event",
                        "weather",
                        "holiday",
                        "promotion",
                        "sale",
                        "seasonal",
                        "external"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/demand_signals/bulk_create": {
      "post": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Create many demand signals in the background",
        "description": "Queues a background job that creates one signal per item and answers\n202 at once; poll `status_url` (GET /bulk_operations/status/{job_id})\nfor progress and per-item errors. Items are checked by the job, not\nhere: an item needs `location_id` (one of the business's locations),\n`signal_type` and `signal_date`, and `name` unless the type is\n\"weather\". `value` is not accepted here — send `impact_factor`.\n\nAn item matches an existing signal when location, date, type and name\nare all equal. `options.conflict_resolution` decides what it does\nthen: `skip` (default — counted as processed, not successful),\n`overwrite` (the item's fields are written onto it), or `fail` (the\nitem fails). `options.validation_mode`: `strict` (default) stops the\nwhole operation at the first failed item — items already written stay\nwritten — and `lenient` records the failure and carries on.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore anything else is checked. A batch with\n`options.conflict_resolution` `overwrite` in which any item carries an\n`active` that does not read as true (false, `\"0\"`, `\"\"`, null)\nswitches existing signals off, so it also asks\n`destructive:scheduling` on top of `write:shifts`, and nothing is\nqueued without it. The caller must also be an admin or super admin of\nthe business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "demand_signals"
                ],
                "properties": {
                  "demand_signals": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/DemandSignalsBulkItem"
                    }
                  },
                  "options": {
                    "$ref": "#/components/schemas/DemandSignalsBulkOptions"
                  }
                }
              },
              "example": {
                "demand_signals": [
                  {
                    "location_id": 12,
                    "signal_type": "holiday",
                    "signal_date": "2026-12-24",
                    "name": "Christmas Eve",
                    "impact_factor": 1.6
                  },
                  {
                    "location_id": 12,
                    "signal_type": "weather",
                    "signal_date": "2026-12-26",
                    "impact_factor": 0.8
                  }
                ],
                "options": {
                  "validation_mode": "lenient"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemandSignalsBulkAccepted"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the batch overwrites signals with `active` off and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was queued.\n- `demand_signals` was missing or empty: `{\"error\": {\"code\": \"validation_failed\", \"message\": \"No demand signals provided\"}}`.\n- The bulk operation could not be saved: `{\"errors\": [\"…\"]}`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/DemandSignalsValidationMessages"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "`service_unavailable` — the job could not be queued (\"Could not\nschedule the bulk operation. Please try again.\"). The operation is\nsaved as `failed` and returned beside the error as\n`bulk_operation`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "bulk_operation": {
                          "$ref": "#/components/schemas/DemandSignalsBulkOperation"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/demand_signals/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Demand signal ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Get a demand signal",
        "description": "One signal of the caller's business, active or not, with its location\nrow.\n\n**Required Scopes:** `read:shifts`. A token without it (or\n`read:own_shifts`, `admin`, or `read:all`) gets `403 insufficient_permissions`\n(\"Required scope: read:shifts\") before the signal is looked up. Any\nmember of the business may read. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The signal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "demand_signal"
                  ],
                  "properties": {
                    "demand_signal": {
                      "$ref": "#/components/schemas/DemandSignalsSignalWithLocation"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no signal with that id in the caller's business (\"Demand signal not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Update a demand signal",
        "description": "Changes any of the fields in `demand_signal`; the rest are left alone.\n`value` is accepted as the older name of `impact_factor` when\n`impact_factor` is blank. A sent `location_id` must be one of the\nbusiness's locations; a blank one is not checked here and fails the\nsave.\n\n**Required Scopes:** `write:shifts`. A token without it (or `admin`)\ngets `403 insufficient_permissions` (\"Required scope: write:shifts\")\nbefore the signal is looked up. Switching a live signal off —\n`active` false, `\"0\"`, `\"\"` or null, anything that does not read as\ntrue, sent to a signal that is on — takes it out of every forecast it\nfed, so it also asks `destructive:scheduling` on top of\n`write:shifts`. Re-sending an off value to a signal already off,\nswitching one on, and every other edit need `write:shifts` alone. The\ncaller must also be an admin or super admin of the business. A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "demand_signal"
                ],
                "properties": {
                  "demand_signal": {
                    "$ref": "#/components/schemas/DemandSignalsSignalInput"
                  }
                }
              },
              "example": {
                "demand_signal": {
                  "name": "Cup final",
                  "impact_factor": 1.5
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated signal, with its location row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "demand_signal"
                  ],
                  "properties": {
                    "demand_signal": {
                      "$ref": "#/components/schemas/DemandSignalsSignalWithLocation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `demand_signal` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the update switches a live signal off and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` — no signal with that id in the caller's business (\"Demand signal not found\").\n- `not_found` — the sent `location_id` is not one of the business's locations (\"Location not found\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The change was not saved; `errors` holds one sentence per failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemandSignalsValidationMessages"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Update a demand signal (PUT)",
        "description": "Same as PATCH /demand_signals/{id}: the same body, gates and\nresponses, including `destructive:scheduling` on top of `write:shifts`\nfor an update that switches a live signal off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "demand_signal"
                ],
                "properties": {
                  "demand_signal": {
                    "$ref": "#/components/schemas/DemandSignalsSignalInput"
                  }
                }
              },
              "example": {
                "demand_signal": {
                  "name": "Cup final",
                  "impact_factor": 1.5
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated signal, with its location row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "demand_signal"
                  ],
                  "properties": {
                    "demand_signal": {
                      "$ref": "#/components/schemas/DemandSignalsSignalWithLocation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — the body has no `demand_signal` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:shifts` (\"Required scope: write:shifts\"), or the update switches a live signal off and the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` — no signal with that id in the caller's business (\"Demand signal not found\").\n- `not_found` — the sent `location_id` is not one of the business's locations (\"Location not found\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The change was not saved; `errors` holds one sentence per failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemandSignalsValidationMessages"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Demand Signals"
        ],
        "summary": "Delete a demand signal",
        "description": "Deletes the signal; it stops feeding the forecasts at its location and\ndate.\n\n**Required Scopes:** `destructive:scheduling` — the area's delete\nlevel, asked instead of `write:shifts` (a token holding only\n`write:shifts` is refused). A token without it (or `admin`) gets\n`403 insufficient_permissions` (\"Required scope:\ndestructive:scheduling\") before the signal is looked up. The caller\nmust also be an admin or super admin of the business. A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted; no body"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:scheduling` (\"Required scope: destructive:scheduling\").\n- `forbidden` — the caller is not an admin or super admin of the business (\"Admin access required\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no signal with that id in the caller's business (\"Demand signal not found\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/export_files": {
      "get": {
        "tags": [
          "Export Files"
        ],
        "summary": "List exports",
        "description": "The exports the caller can see (their own, or every export in the\nbusiness for an admin), newest first, paginated.\n\n**Required Scopes:** none for the list itself, but every row carries\nits download link, so a token sees only the exports whose type's\nscope it holds in the wide form (`read:users`, `read:shifts`,\n`read:attendance`, `read:leave_requests`, or `admin`); the others are\nleft out of the rows and the counts, never refused. A browser session\nsees every type. The caller must be a manager, admin or super admin of\nthe business. Shifts and shift-assignments exports are left out of\nthe rows and the counts while Shifts & Scheduling is off for the\nbusiness or not open to the caller — the same rule that answers 404\nwhen one is read.\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": "Exports per page. Clamped to 1..100; a blank or non-numeric value reads as 0 and is clamped to 1. Missing, or sent as a list, reads as 10.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of exports",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "export_files",
                    "meta"
                  ],
                  "properties": {
                    "export_files": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ExportFilesExportFile"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "current_page",
                        "total_pages",
                        "total_count"
                      ],
                      "properties": {
                        "current_page": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "total_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — the caller is not a manager, admin or super admin of the business (\"Exporting business-wide records requires manager access\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Start an export",
        "description": "Starts an export of `export_file.export_type` in `export_file.format`\n(default csv) and answers 201 at once with `status_url` to poll. The\ncaller is recorded as the export's requester. No export takes\n`filters` or `options` — the file holds every row of its type — and a\nnon-empty one, at the top level or inside `export_file`, is refused\nwith `422 unsupported_parameter` before anything is saved. The typed routes (POST /export_files/shifts,\n/users, /attendance_records, /leave_requests, /shift_assignments) start\nthe same exports.\n\n**Required Scopes:** the export type's — `read:users` for `users` /\n`employees`, `read:shifts` for `shifts` / `shift_assignments` /\n`assignments`, `read:attendance` for `attendance_records`,\n`read:leave_requests` for `leave_requests` — in the wide form only\n(or `admin`). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:…\") and nothing is saved. The caller must be a\nmanager, admin or super admin, and an admin or super admin for\n`users` / `employees`; `shifts` / `shift_assignments` / `assignments`\nalso need Shifts & Scheduling enabled and open to the caller. A browser session is asked no\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "export_file"
                ],
                "properties": {
                  "export_file": {
                    "type": "object",
                    "required": [
                      "export_type"
                    ],
                    "properties": {
                      "export_type": {
                        "type": "string",
                        "enum": [
                          "shifts",
                          "users",
                          "attendance_records",
                          "leave_requests",
                          "shift_assignments",
                          "assignments",
                          "employees"
                        ],
                        "description": "`employees` and `assignments` are older names for `users` and `shift_assignments`."
                      },
                      "format": {
                        "type": "string",
                        "enum": [
                          "csv",
                          "json",
                          "xlsx"
                        ],
                        "default": "csv"
                      }
                    }
                  }
                }
              },
              "example": {
                "export_file": {
                  "export_type": "leave_requests",
                  "format": "xlsx"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "400": {
            "description": "`parameter_missing` — the body has no `export_file` object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\"); or the type is `users` / `employees` and the caller is not an admin or super admin (\"Exporting the employee directory requires administrator access\"); or the type is `shifts` / `shift_assignments` / `assignments` and Shifts & Scheduling is off (\"Shifts & Scheduling is not enabled for this business\") or not open to the caller (\"You don't have access to Shifts & Scheduling. Please contact your administrator.\").\n- `insufficient_permissions` — the token lacks the type's scope (\"Required scope: read:users\", \"read:shifts\", \"read:attendance\" or \"read:leave_requests\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/users": {
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Export the employee directory",
        "description": "Starts an export of every person in the business — ID, Name, Email,\nJob Title, Department, Hire Date, Active — and answers 201 at once\nwith `status_url` to poll.\n\n**Required Scopes:** `read:users` (wide; or `admin`). A token without\nit gets `403 insufficient_permissions` (\"Required scope: read:users\")\nand nothing is saved. The caller must be an admin or super admin of\nthe business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportFilesTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\"), or is a manager but not an admin or super admin (\"Exporting the employee directory requires administrator access\").\n- `insufficient_permissions` — the token lacks `read:users` (\"Required scope: read:users\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/shifts": {
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Export shifts",
        "description": "Starts an export of the business's shifts — ID, Name, Start Time, End\nTime, Status, Shift Type, Location, Required Staff, Published, Notes —\nand answers 201 at once with `status_url` to poll. The file holds only\nthe shifts the requester oversees as a scheduler, worked out when the\nexport is started (every shift for a caller who oversees all of\nthem).\n\n**Required Scopes:** `read:shifts`, wide (or `admin`) —\n`read:own_shifts` does not stand in. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:shifts\") and\nnothing is saved. The caller must be a manager, admin or super admin,\nand Shifts & Scheduling must be enabled for the business and open to\nthe caller. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportFilesTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\"), or Shifts & Scheduling is off (\"Shifts & Scheduling is not enabled for this business\") or not open to the caller (\"You don't have access to Shifts & Scheduling. Please contact your administrator.\").\n- `insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/shift_assignments": {
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Export shift assignments",
        "description": "Starts an export of every shift assignment in the business — ID,\nEmployee, Shift, Shift Start, Shift End, Status, Assignment Type,\nScheduled Hours, Hours Worked, Total Cost — and answers 201 at once\nwith `status_url` to poll.\n\n**Required Scopes:** `read:shifts`, wide (or `admin`) —\n`read:own_shifts` does not stand in. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:shifts\") and\nnothing is saved. The caller must be an admin or super admin (the file\ncarries each assignment's per-employee cost), and Shifts & Scheduling\nmust be enabled for the business and open to the caller. A browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportFilesTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\"), is a manager but not an admin (\"Shift assignment exports, with their per-employee cost, are limited to administrators. Managers can export their own people's assignments from Shifts & Scheduling › Reports › Shift Assignments.\"), or Shifts & Scheduling is off (\"Shifts & Scheduling is not enabled for this business\") or not open to the caller (\"You don't have access to Shifts & Scheduling. Please contact your administrator.\").\n- `insufficient_permissions` — the token lacks `read:shifts` (\"Required scope: read:shifts\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/attendance_records": {
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Export attendance records",
        "description": "Starts an export of every attendance record in the business — ID,\nEmployee, Shift, Check In, Check Out, Status, Work Mode, Punch Source,\nNotes — and answers 201 at once with `status_url` to poll.\n\n**Required Scopes:** `read:attendance`, wide (or `admin`) —\n`read:own_attendance` does not stand in. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:attendance\") and\nnothing is saved. The caller must be an admin or super admin of the\nbusiness, as for the web's bulk attendance export; sample punches and\nunapproved missing-punch requests are left out. A browser session is\nasked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportFilesTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not an admin or super admin (\"Attendance exports are limited to administrators. Managers can export their own people's time cards from Time & Attendance › All Records › Time Card Report.\").\n- `insufficient_permissions` — the token lacks `read:attendance` (\"Required scope: read:attendance\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/leave_requests": {
      "post": {
        "tags": [
          "Export Files"
        ],
        "summary": "Export leave requests",
        "description": "Starts an export of every leave request in the business — ID,\nEmployee, Leave Type, Start Date, End Date, Half Day, Hours Requested\n(the hours the request takes), Status (as the screens label it:\nPending approval, Special approval, Approved, Denied, Cancelled),\nRequested On, Notes — and answers 201 at once with `status_url` to poll.\n\n**Required Scopes:** `read:leave_requests`, wide (or `admin`) —\n`read:own_leave_requests` does not stand in. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:leave_requests\")\nand nothing is saved. The caller must be an admin or super admin of\nthe business. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportFilesTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/ExportFilesQueued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not an admin or super admin (\"Leave request exports are limited to administrators. Managers can export their own people's requests from Leave Management › Leave Requests › Export.\").\n- `insufficient_permissions` — the token lacks `read:leave_requests` (\"Required scope: read:leave_requests\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ExportFilesNotQueued"
          },
          "503": {
            "$ref": "#/components/responses/ExportFilesNotScheduled"
          }
        }
      }
    },
    "/export_files/status/{job_id}": {
      "get": {
        "tags": [
          "Export Files"
        ],
        "summary": "Progress of an export",
        "description": "The export started with this `job_id` and how far it has got.\n`progress` is 0 until the file is ready and 100 once it is — show an\nindeterminate state while `status` is `processing`.\n\n**Required Scopes:** the scope of the export's type (see POST\n/export_files), wide only (or `admin`). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:…\"). The caller\nmust be a manager, admin or super admin, and must be able to see the\nexport (their own, or any for an admin). A browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "The `job_id` the creating call returned.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export and its progress",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "export_file",
                    "status",
                    "progress",
                    "file_size",
                    "completed_at",
                    "download_url"
                  ],
                  "properties": {
                    "export_file": {
                      "$ref": "#/components/schemas/ExportFilesExportFile"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "processing",
                        "completed",
                        "failed",
                        "cancelled"
                      ]
                    },
                    "progress": {
                      "type": "integer",
                      "enum": [
                        0,
                        100
                      ]
                    },
                    "file_size": {
                      "type": "string",
                      "nullable": true,
                      "description": "Human-readable size (\"12.4 KB\") once the file exists, else null."
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "download_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "The path of GET /export_files/{id}/download once the file can be downloaded (completed and not expired), else null.",
                      "example": "/api/v1/export_files/41/download"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\").\n- `insufficient_permissions` — the token lacks the export type's scope (\"Required scope: read:…\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` (\"Export file not found\") — no export the caller can see\nhas that `job_id`, or it is a shifts or shift-assignments export\nand Shifts & Scheduling is off for the business or not open to the\ncaller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/export_files/{id}": {
      "get": {
        "tags": [
          "Export Files"
        ],
        "summary": "Get an export",
        "description": "One export the caller can see.\n\n**Required Scopes:** the scope of the export's type (see POST\n/export_files), wide only (or `admin`). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:…\"). The caller\nmust be a manager, admin or super admin. A browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ExportFilesId"
          }
        ],
        "responses": {
          "200": {
            "description": "The export",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "export_file"
                  ],
                  "properties": {
                    "export_file": {
                      "$ref": "#/components/schemas/ExportFilesExportFile"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\").\n- `insufficient_permissions` — the token lacks the export type's scope (\"Required scope: read:…\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` (\"Export file not found\") — no export the caller can see\nhas that id, or it is a shifts or shift-assignments export and\nShifts & Scheduling is off for the business or not open to the\ncaller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/export_files/{id}/download": {
      "get": {
        "tags": [
          "Export Files"
        ],
        "summary": "Get a link to a finished export's file",
        "description": "Answers with a signed link to the file, not the file itself. The link\nis a relative path, good for one hour from this call; the file is kept\nfor seven days after the export completes (`expires_at`). Ask again\nfor a fresh link.\n\n**Required Scopes:** the scope of the export's type (see POST\n/export_files), wide only (or `admin`). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:…\"). The caller\nmust be a manager, admin or super admin. A browser session is asked\nno scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ExportFilesId"
          }
        ],
        "responses": {
          "200": {
            "description": "The download link and the file's details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "download_url",
                    "file_name",
                    "content_type",
                    "file_size",
                    "file_size_formatted",
                    "expires_at"
                  ],
                  "properties": {
                    "download_url": {
                      "type": "string",
                      "description": "A signed path to the file, valid for one hour.",
                      "example": "/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsi…/leave_requests_20261010_101500.xlsx?disposition=attachment"
                    },
                    "file_name": {
                      "type": "string",
                      "example": "leave_requests_20261010_101500.xlsx"
                    },
                    "content_type": {
                      "type": "string",
                      "example": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    },
                    "file_size": {
                      "type": "integer",
                      "description": "Bytes."
                    },
                    "file_size_formatted": {
                      "type": "string",
                      "example": "12.4 KB"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `forbidden` — the caller is not a manager, admin or super admin (\"Exporting business-wide records requires manager access\").\n- `insufficient_permissions` — the token lacks the export type's scope (\"Required scope: read:…\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportFilesForbidden"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` (\"Export file not found\") — no export the caller can see\nhas that id, or it is a shifts or shift-assignments export and\nShifts & Scheduling is off for the business or not open to the\ncaller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`gone` — \"Export file has expired and is no longer available\"; `expires_at` is returned beside the error.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` — \"Export file is not ready for download\": the\nexport has not completed (its `status` is returned beside the\nerror), or it completed without a file.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "processing",
                            "failed",
                            "cancelled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/frontline_execution/locations": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The \"Which locations\" store picker for the intake-request form (with search + pagination)",
        "description": "Returns the tenant's active physical sites as `{ id, name }`, ordered by name —\nthe store list the \"New request\" form's \"Which locations\" multi-select offers.\nThe list is not span-bounded: every member sees every store (a store can ask\nfor store-facing work too).\n\n`q` narrows by name (case-insensitive substring, LIKE-escaped) — the server-side\nform of the web picker's client-side typeahead. `page` and `limit` page the\nresult; the standard `meta` envelope carries the true total, so every site is\nreachable even in a tenant with more stores than the web picker's render cap.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow the list by location name (case-insensitive substring).",
            "example": "market"
          },
          {
            "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 active physical sites for the picker, ordered by name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineExecutionLocationsResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks an execution read scope (`insufficient_permissions`), or the Campaigns surface is off (`surface_disabled`)."
          }
        }
      }
    },
    "/frontline_execution/config": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The intake-request form's config — the category dropdown options",
        "description": "Returns the option lists a client needs to render the \"New request\" form. Today\nthat is `categories`: the tenant's active publication taxonomy (\"MAP Retail\",\n\"Planogram\", \"Safety\") a requester tags their ask with, reusing the SAME\nactive-only, ordered scope the web request form's category select binds to — so\nthe app and the browser can never offer a different set of categories.\n\nRetired (inactive) categories are excluded: they stay on the campaigns they\nalready label but are never offered for something new. Every member reaches this\n(a store can ask for store-facing work too); it is not span-bounded. The list is\na small, bounded taxonomy, so it is returned unpaginated.\n",
        "responses": {
          "200": {
            "description": "The intake-request form's config (the category dropdown options).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineExecutionRequestFormConfig"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks an execution read scope (`insufficient_permissions`), or the Campaigns surface is off (`surface_disabled`)."
          }
        }
      }
    },
    "/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
                        },
                        "target_locations": {
                          "type": "array",
                          "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "name": {
                                "type": "string",
                                "example": "Store 042 - Downtown"
                              }
                            }
                          }
                        },
                        "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:frontline_execution` scope.\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
                        },
                        "target_locations": {
                          "type": "array",
                          "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "name": {
                                "type": "string",
                                "example": "Store 042 - Downtown"
                              }
                            }
                          }
                        },
                        "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/requests/{id}/withdraw": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Withdraw your own pending intake request",
        "description": "Withdraws the request: it moves to `withdrawn` and the caller is stamped\nas `decided_by` / `decided_at`. Any live (pending or escalated) approval\nrequest a configured workflow opened for it is cancelled, so reviewers\nstop seeing an abandoned ask. Nobody is notified — nothing was decided.\n\nOnly the request's submitter may withdraw it, and only while it is still\npending — exactly the condition `viewer.can_withdraw` on the detail\nendpoint reports, so a client that honours that flag never sees a refusal.\n\nReturns the request in the same `{request, viewer}` shape the detail\nendpoint returns — now `withdrawn`, so `viewer.can_withdraw` is false.\nNo request body.\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:frontline_execution`\nscope. A personal token cannot hold it: do this in MangoApps instead.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The intake request (Execution::CampaignRequest) to withdraw."
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn. Returns the 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
                        },
                        "target_locations": {
                          "type": "array",
                          "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 42
                              },
                              "name": {
                                "type": "string",
                                "example": "Store 042 - Downtown"
                              }
                            }
                          }
                        },
                        "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
                        }
                      }
                    }
                  }
                },
                "example": {
                  "request": {
                    "id": 412,
                    "title": "Re-check cold chain after firmware fix",
                    "status": "withdrawn",
                    "decided_by": {
                      "id": 88,
                      "name": "Priya Shah"
                    },
                    "decided_at": "2026-09-24T10:15:02Z",
                    "decline_reason": null,
                    "under_engine_review": false,
                    "campaign": null
                  },
                  "viewer": {
                    "is_requester": true,
                    "is_gatekeeper": false,
                    "can_approve": false,
                    "can_decline": false,
                    "can_withdraw": false
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The tenant has used its monthly API allowance (`api_credits_exhausted`)."
          },
          "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 is not this request's submitter (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "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`, `invalid`, or (withdraw only) `forbidden`.",
                          "example": "reason_required"
                        },
                        "message": {
                          "type": "string",
                          "example": "Say why — it's the only thing the person who asked for this will see."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "error": {
                    "code": "forbidden",
                    "message": "You can only withdraw your own requests."
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such request in this business (`not_found`)."
          },
          "409": {
            "description": "`already_decided` — the request is no longer pending (approved, declined, already withdrawn, or a concurrent decision won the race).",
            "content": {
              "application/json": {
                "schema": {
                  "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`, `invalid`, or (withdraw only) `forbidden`.",
                          "example": "reason_required"
                        },
                        "message": {
                          "type": "string",
                          "example": "Say why — it's the only thing the person who asked for this will see."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "error": {
                    "code": "already_decided",
                    "message": "That request has already been decided."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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`, `campaign_closed` (the campaign was closed — this work is no longer needed), 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. `notified` in the\nresponse says whether that notice went out — `false` when the worker\ncould not be told (they left, or the app's door refuses them), so tell\nthem directly or hand the redo to someone else. It is absent when a\nconfigured approval workflow decided the send-back, which reports none.\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`),\nthe caller submitted it themselves (`self_review`), or the campaign was\nclosed (`campaign_closed` — accept the work or leave it; a closed\ncampaign's work is not sent back for a redo).\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:frontline_execution`\nscope.\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"
                    },
                    "notified": {
                      "type": "boolean",
                      "description": "Whether the worker's notice went out. Absent for a send-back a configured approval workflow decided."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible (`app_disabled` / `forbidden`), the token lacks `destructive:frontline_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), `self_review` (the reviewer did the work) or `campaign_closed` (the campaign was closed).",
            "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), `in_review` (waiting on a reviewer) or `campaign_closed` (the campaign was closed — this work is no longer needed).",
            "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), `not_assigned` (nobody holds it) or `campaign_closed` (the campaign was closed — this work is no longer needed).",
            "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\nA reason is REQUIRED — a hand-off is an override of the resolver's pick, and\nthe reason lands on the work's history and is the one thing the previous\nholder reads. Send `reason_code` (preferred): with a code, `reason` becomes\nan optional note, except for `other`, which needs one. A client that sends\nonly `reason` (at least 3 characters) still works, but its hand-off carries\nno code and therefore informs no Muscle Memory lesson.\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"
                ],
                "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_code": {
                    "type": "string",
                    "enum": [
                      "not_scheduled",
                      "on_leave",
                      "skills",
                      "workload",
                      "other"
                    ],
                    "description": "Why the work is being handed off, as a choice: not scheduled or off shift, on leave or out sick, needs different skills or training, already has too much on, or something else. This is what Muscle Memory counts; the note is never interpreted.",
                    "example": "on_leave"
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 3,
                    "description": "A note for the work's assignment history, shown to the previous holder. Required when `reason_code` is `other` or absent.",
                    "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`); no usable reason — an unknown `reason_code`, `other` with no note, or neither a code nor a note of 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`); the campaign was closed (`campaign_closed` — this work is no longer needed); 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`)."
          }
        }
      }
    },
    "/meal_compliance/dashboard": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The Meal Compliance dashboard (tiles, withdrawals, recent activity, behavior changes)",
        "description": "Every figure is relationship-scoped exactly as on the web Dashboard:\nHR admins, business admins and Meal Compliance app admins see the whole\norganization (`scope: all`); a line manager sees their direct reports and\nthemself (`scope: team`); anyone else sees their own records (`scope: self`).\nA token holding only `read:own_meal_compliance` is answered as `self`\nwhatever the caller's role.\n\nOn a `self` dashboard, sample waivers and sample break records are hidden\nfrom a real person, and a draft a manager discarded is left out — the\nsame rules as My Breaks.\n\nThe response costs a fixed number of queries whatever the number of\nwaivers or people.\n",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 4
            },
            "description": "How many rows of Recent activity to return (clamped 1–10). The default, 4, is the number the mobile Dashboard lists; 10 is the web card's length."
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the words this response writes (tile labels and captions, status labels, row descriptions). Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "The dashboard for the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceDashboard"
                },
                "example": {
                  "scope": "all",
                  "tiles": [
                    {
                      "key": "active_waivers",
                      "label": "Active waivers",
                      "value": 5,
                      "caption": "71% of waiving employees covered",
                      "icon": "file-signature",
                      "tone": "success",
                      "coverage": {
                        "percentage": 71,
                        "waiving_employees": 7,
                        "covered": 5,
                        "not_covered": 2,
                        "not_covered_label": "See 2 not covered"
                      }
                    },
                    {
                      "key": "pending_signatures",
                      "label": "Pending signatures",
                      "value": 3,
                      "caption": "2 awaiting employee · 1 waiting on a manager",
                      "icon": "clock",
                      "tone": "warning",
                      "awaiting_employee": 2,
                      "waiting_on_manager": 1
                    },
                    {
                      "key": "behavior_alerts",
                      "label": "Behavior alerts",
                      "value": 3,
                      "caption": "Last 7 days",
                      "icon": "bell",
                      "tone": "info",
                      "window_days": 7
                    },
                    {
                      "key": "invalidated",
                      "label": "Invalidated",
                      "value": 1,
                      "caption": "Ended or declined, not in force",
                      "icon": "triangle-exclamation",
                      "tone": "danger"
                    }
                  ],
                  "withdrawals": {
                    "total": 1,
                    "window_days": 7,
                    "items": [
                      {
                        "waiver_id": 412,
                        "kind": "withdrawn",
                        "description": "Withdrew their meal-break waiver",
                        "ended_at": "2026-10-21T09:12:44Z",
                        "employee": {
                          "id": 5120,
                          "name": "Omar Haddad",
                          "initials": "OH",
                          "avatar_url": "https://ui-avatars.com/api/?name=Omar%20Haddad"
                        }
                      }
                    ]
                  },
                  "recent_activity": {
                    "limit": 4,
                    "items": [
                      {
                        "waiver_id": 418,
                        "status": "pending_signature",
                        "status_label": "Pending Signature",
                        "tone": "warning",
                        "location": {
                          "id": 31,
                          "name": "Downtown Store"
                        },
                        "updated_at": "2026-10-21T08:40:02Z",
                        "employee": {
                          "id": 5133,
                          "name": "Grace Liu",
                          "initials": "GL",
                          "avatar_url": "https://ui-avatars.com/api/?name=Grace%20Liu"
                        }
                      }
                    ]
                  },
                  "behavior_changes": {
                    "total": 1,
                    "window_days": 7,
                    "items": [
                      {
                        "id": 902,
                        "previous_pattern": "taking",
                        "previous_pattern_label": "Taking",
                        "detected_pattern": "waiving",
                        "detected_pattern_label": "Waiving",
                        "summary": "Changed from Taking to Waiving",
                        "detected_at": "2026-10-19T06:00:00Z",
                        "employee": {
                          "id": 5133,
                          "name": "Grace Liu",
                          "initials": "GL",
                          "avatar_url": "https://ui-avatars.com/api/?name=Grace%20Liu"
                        }
                      }
                    ]
                  },
                  "permissions": {
                    "can_create_waiver": true,
                    "can_view_waivers": true,
                    "can_view_gaps": true,
                    "gaps_web_url": "https://acme.workforce.mangoapps.com/apps/meal-compliance/waivers/gaps"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance` (`insufficient_permissions`), the app is not on for the organization (`app_disabled`), or the caller may not open it (`app_access_denied`)."
          },
          "404": {
            "description": "The Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      }
    },
    "/meal_compliance/waivers": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The Waivers register (status filters with counts, search, location, sort, paginated)",
        "description": "Every row is relationship-scoped exactly as on the web register:\nHR admins, business admins and Meal Compliance app admins see the whole\norganization (`scope: all`); a line manager sees their direct reports and\nthemself (`scope: team`); anyone else sees their own waivers\n(`scope: self`). A token holding only `read:own_meal_compliance` is\nanswered as `self` whatever the caller's role. On a `self` register a\ndraft a manager discarded is left out, and sample waivers are hidden\nfrom a real person — the same rules as the Dashboard and My Breaks.\n\n**Status filters.** `filters` always carries All, Active, Pending\nSignature, Invalidated, Expired and Draft with their counts, and Not\nYet In Effect while it holds a waiver or is selected — the web\nregister's chips. Statuses are read the way each row's badge reads:\n`active` is in force today, `expired` includes a waiver whose term ended\nbefore the 4-hourly sweep reached it, and `not_yet_in_effect` is a\nsigned waiver whose term starts later. The counts honour `search` and\n`location_id` but not `status`, so they always add up to All and a chip\nnever promises rows the list will not show.\n\nThe response costs a fixed number of queries whatever the number of\nwaivers or people.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "active",
                "pending_signature",
                "invalidated",
                "expired",
                "draft",
                "not_yet_in_effect"
              ],
              "default": "all"
            },
            "description": "The status chip. An unrecognised value lists every waiver and sets `meta.filter_ignored`."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Matches the employee's first, last, preferred or full name — and their email only where the People directory shows this caller email."
          },
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "One of `locations[].id`. A location of another organization matches nothing."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "valid_until",
                "employee"
              ]
            },
            "description": "Omitted, the most recently updated waivers come first. `valid_until` puts waivers with no expiry last."
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "The sort direction; ignored without `sort`."
          },
          {
            "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": 100,
              "default": 25
            },
            "description": "Clamped to 1–100. 25 is the web register's page."
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the words this response writes (chip, status, behavior and tag labels). Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the register for the caller. The `X-Total-Count`, `X-Total-Pages`, `X-Current-Page` and `X-Per-Page` headers repeat `meta`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaivers"
                },
                "example": {
                  "scope": "all",
                  "status": "all",
                  "search": null,
                  "location_id": null,
                  "sort": null,
                  "dir": null,
                  "filters": [
                    {
                      "key": "all",
                      "label": "All",
                      "count": 12,
                      "selected": true
                    },
                    {
                      "key": "active",
                      "label": "Active",
                      "count": 4,
                      "selected": false
                    },
                    {
                      "key": "pending_signature",
                      "label": "Pending Signature",
                      "count": 3,
                      "selected": false
                    },
                    {
                      "key": "invalidated",
                      "label": "Invalidated",
                      "count": 1,
                      "selected": false
                    },
                    {
                      "key": "expired",
                      "label": "Expired",
                      "count": 2,
                      "selected": false
                    },
                    {
                      "key": "draft",
                      "label": "Draft",
                      "count": 2,
                      "selected": false
                    }
                  ],
                  "locations": [
                    {
                      "id": 41,
                      "name": "North Warehouse"
                    },
                    {
                      "id": 42,
                      "name": "Seattle Distribution Center"
                    }
                  ],
                  "items": [
                    {
                      "id": 904,
                      "status": "active",
                      "status_label": "Active",
                      "tone": "success",
                      "employee": {
                        "id": 5133,
                        "name": "Omar Haddad",
                        "email": "omar.haddad@acme.com",
                        "initials": "OH",
                        "avatar_url": "https://ui-avatars.com/api/?name=Omar%20Haddad",
                        "active_member": true
                      },
                      "behavior": {
                        "state": "waiving",
                        "label": "Waiving",
                        "icon": "forward"
                      },
                      "elections_summary": "1st meal period: Waived · 2nd meal period: Waived",
                      "jurisdiction_name": "California",
                      "location": {
                        "id": 41,
                        "name": "North Warehouse"
                      },
                      "valid_from": "2026-04-15",
                      "valid_until": {
                        "date": "2026-10-25",
                        "label": null,
                        "days_until_expiry": 4,
                        "tag": {
                          "kind": "expiring",
                          "label": "Expires in 4 days",
                          "tone": "warning"
                        }
                      },
                      "is_sample_data": false,
                      "created_at": "2026-04-10T16:02:11Z",
                      "updated_at": "2026-10-18T09:14:00Z"
                    }
                  ],
                  "meta": {
                    "total_count": 12,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 25,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "permissions": {
                    "can_create_waiver": true,
                    "can_export": true,
                    "export_web_url": "https://acme.workforce.mangoapps.com/apps/meal-compliance/waivers/export.csv?status=active"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance` (`insufficient_permissions`), the app is not on for the organization (`app_disabled`), or the caller may not open it (`app_access_denied`)."
          },
          "404": {
            "description": "The Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      },
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a waiver, as a draft",
        "description": "The web New Waiver form's save (MealCompliance::WaiverCreation — the\nform's own writer). The new waiver is a DRAFT: not in force, and the\nemployee has seen nothing; the answer is its page — the same body as\n`GET /meal_compliance/waivers/{id}` — with the next step (Send for\nSignature, Activate) in its `actions`.\n\nOnly a caller who may create waivers (HR, an admin, a Meal Compliance\napp admin or a line manager) with the wide `write:meal_compliance`\nscope reaches it; a line manager only for their own direct reports —\nanyone else named is a `user_id` field error, as on the web form. A\nwaiver already on its way for the employee (a draft, one out for\nsignature, one signed that has not started) holds the save with 409\nuntil `create_beside_in_progress` names it.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MealComplianceWaiverCreateRequest"
              },
              "example": {
                "user_id": 5133,
                "behavior_state": "waiving",
                "location_id": 41,
                "jurisdiction_id": 7,
                "valid_from": "2026-10-22",
                "valid_until": "2027-04-20",
                "notes": "Signed at orientation."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved. `message` names the next step; `tone` is `warning` when the per-period elections could not be recorded — `message` then says so too: the draft reflects no waived period and, having no edit route, should be discarded and created again.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "message": {
                          "type": "string",
                          "example": "Draft saved for Omar Haddad. It isn't in force until it's signed — send it for signature or activate it."
                        },
                        "tone": {
                          "type": "string",
                          "enum": [
                            "success",
                            "warning"
                          ]
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/MealComplianceWaiverDetails"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller may not create waivers, or the token is read-only or own-tier (`forbidden`); or the token lacks the scope (`insufficient_permissions`), or the app is not open to the caller."
          },
          "409": {
            "description": "A waiver is already on its way for this employee (`in_progress_waiver`). `error.details.in_progress_waiver` names it and `error.details.create_override` says how to save beside it.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "in_progress_waiver",
                    "message": "Omar Haddad already has a waiver on its way: waiver #918 (draft). …",
                    "details": {
                      "in_progress_waiver": {
                        "id": 918,
                        "status": "draft",
                        "status_label": "Draft"
                      },
                      "create_override": {
                        "param": "create_beside_in_progress",
                        "value": 918,
                        "label": "Create another waiver anyway"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Field errors (`errors[]` of `{ field, message, code }`): no employee, someone outside a line manager's reach, an employee of another organization or no longer active in it, a missing or unrecognised Behavior State where it is chosen, `elections` that is not an object, a Location or Jurisdiction that is not active here, a Valid Until before today or before Valid From, Notes over 2,000 characters.",
            "content": {
              "application/json": {
                "example": {
                  "errors": [
                    {
                      "field": "user_id",
                      "message": "must be one of your direct reports. Ask an HR or app administrator to create waivers for other employees.",
                      "code": "validation_error"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/search": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Search waivers by employee name or email",
        "description": "The Search screen of the Waivers tab. Matches `q` against the\nemployee's first, last, preferred and full name, and their email only\nwhere the People directory shows this caller email\n(`searchable_by` says which) — the web register's own search box, over\nthe same relationship scope as `GET /meal_compliance/waivers`\n(`scope: all | team | self`; a `read:own_meal_compliance` token is\nalways `self`). A waiver never matches on its location, jurisdiction\nor status, and a `status`, `location_id` or `sort` parameter is\nignored: the results are every status, most recently updated first.\n\nA blank `q` is the idle screen and reads no waiver. The response\ncarries the words the screen draws in the caller's language: the\nfield's placeholder, the results line (\"3 results\") and the idle or\nno-results empty state.\n\nThe response costs a fixed number of queries whatever the number of\nmatches.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "Marcus"
            },
            "description": "The employee's name (any part, case-insensitive) or email. Trimmed; blank, or a list-shaped value, is the idle screen."
          },
          {
            "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": 100,
              "default": 25
            },
            "description": "Clamped to 1–100."
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the words this response writes. Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of matches. With a query, the `X-Total-Count`, `X-Total-Pages`, `X-Current-Page` and `X-Per-Page` headers repeat `meta`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverSearch"
                },
                "examples": {
                  "results": {
                    "summary": "Matches for \"Marcus\"",
                    "value": {
                      "scope": "all",
                      "query": "Marcus",
                      "searchable_by": [
                        "name",
                        "email"
                      ],
                      "placeholder": "Search by employee name or email",
                      "results_label": "1 result",
                      "empty_state": null,
                      "items": [
                        {
                          "id": 911,
                          "status": "pending_signature",
                          "status_label": "Pending Signature",
                          "tone": "warning",
                          "subtitle": "Meal Waiver (California) · North Warehouse",
                          "employee": {
                            "id": 5140,
                            "name": "Marcus Lee",
                            "email": "marcus.lee@acme.com",
                            "initials": "ML",
                            "avatar_url": "https://ui-avatars.com/api/?name=Marcus%20Lee",
                            "active_member": true
                          },
                          "behavior": {
                            "state": "waiving",
                            "label": "Waiving",
                            "icon": "forward"
                          },
                          "elections_summary": "1st meal period: Waived · 2nd meal period: Taking",
                          "jurisdiction_name": "California",
                          "location": {
                            "id": 41,
                            "name": "North Warehouse"
                          },
                          "valid_from": "2026-10-01",
                          "valid_until": {
                            "date": "2027-03-30",
                            "label": null,
                            "days_until_expiry": 160,
                            "tag": null
                          },
                          "is_sample_data": false,
                          "created_at": "2026-09-28T16:02:11Z",
                          "updated_at": "2026-10-19T09:14:00Z"
                        }
                      ],
                      "meta": {
                        "total_count": 1,
                        "total_pages": 1,
                        "current_page": 1,
                        "per_page": 25,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  },
                  "idle": {
                    "summary": "Nothing typed yet",
                    "value": {
                      "scope": "team",
                      "query": null,
                      "searchable_by": [
                        "name"
                      ],
                      "placeholder": "Search by employee name",
                      "results_label": null,
                      "empty_state": {
                        "kind": "idle",
                        "title": "Search waivers",
                        "body": "Find a waiver by the employee's name."
                      },
                      "items": [],
                      "meta": {
                        "total_count": 0,
                        "total_pages": 0,
                        "current_page": 1,
                        "per_page": 25,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  },
                  "no_results": {
                    "summary": "No match",
                    "value": {
                      "scope": "all",
                      "query": "Zelda",
                      "searchable_by": [
                        "name",
                        "email"
                      ],
                      "placeholder": "Search by employee name or email",
                      "results_label": null,
                      "empty_state": {
                        "kind": "no_results",
                        "title": "No waivers for “Zelda”",
                        "body": "Check the spelling, or search by the employee's email."
                      },
                      "items": [],
                      "meta": {
                        "total_count": 0,
                        "total_pages": 0,
                        "current_page": 1,
                        "per_page": 25,
                        "has_next_page": false,
                        "has_prev_page": false
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance` (`insufficient_permissions`), the app is not on for the organization (`app_disabled`), or the caller may not open it (`app_access_denied`)."
          },
          "404": {
            "description": "The Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      }
    },
    "/weekly_reports/current": {
      "get": {
        "tags": [
          "Weekly Reports"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "This period's report",
        "description": "The caller's report for the current period — or, before anything is\nwritten, `report: null` with the period and whether this person is\nasked to write one at all (`asked`: a report template applies to them).\nRequires `read:weekly_reports`.\n",
        "responses": {
          "200": {
            "description": "The current report, the period and whether one is asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report",
                    "period",
                    "asked"
                  ],
                  "properties": {
                    "report": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/WeeklyReport"
                        }
                      ],
                      "nullable": true,
                      "description": "Null until the caller starts this period's report."
                    },
                    "period": {
                      "type": "object",
                      "required": [
                        "start",
                        "end",
                        "due_at"
                      ],
                      "properties": {
                        "start": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-10-26"
                        },
                        "end": {
                          "type": "string",
                          "format": "date",
                          "example": "2026-11-01"
                        },
                        "due_at": {
                          "type": "string",
                          "format": "date-time",
                          "example": "2026-10-30T17:00:00-07:00"
                        }
                      }
                    },
                    "asked": {
                      "type": "boolean",
                      "description": "Whether a report template applies to the caller — false means nobody asks them for one.",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled`, `app_forbidden`, or `insufficient_permissions`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/weekly_reports/current/items": {
      "post": {
        "tags": [
          "Weekly Reports"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Add a line to this period's report",
        "description": "Adds one line, starting the report when there is none. `win: true`\nputs it under Wins (the template's first section if it renamed Wins);\notherwise `section_key`, and with neither, under Done. A line in a plan\nsection starts as a plan (`plan_outcome: open`) the next period asks\nabout; one under \"Could use a hand with\" (`help`) is an open ask\n(`help_status: open`). A grouped section takes its lines in the form,\nnot here. Adding to a shared report marks it edited. Requires\n`write:weekly_reports`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "The line, in the writer's own words (trimmed; capped at the item limit).",
                    "example": "Closed the Q4 vendor review"
                  },
                  "section_key": {
                    "type": "string",
                    "description": "One of the report's `sections[].key`. Ignored when `win` is true.",
                    "example": "done"
                  },
                  "win": {
                    "type": "boolean",
                    "default": false,
                    "description": "Put the line under Wins."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The line was added.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item",
                    "report"
                  ],
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/WeeklyReportItem"
                    },
                    "report": {
                      "$ref": "#/components/schemas/WeeklyReport"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled`, `app_forbidden`, `insufficient_permissions`, or the report is not the caller's own / a service account acting as them",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid` — the week is skipped, the section does not exist or is grouped, or `text` is blank. `error.message` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/weekly_reports/current/skip": {
      "post": {
        "tags": [
          "Weekly Reports"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Skip this period",
        "description": "Marks this period's report skipped (starting it first when there is\nnone), with an optional `note` only the writer sees (stored, not sent\nback on the report). Only a draft can be skipped; any plan carried into\nit goes back to unanswered. Requires `write:weekly_reports`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Why, for the writer's own eyes.",
                    "example": "On leave this week"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report, now skipped.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/WeeklyReport"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled`, `app_forbidden`, `insufficient_permissions`, or a service account acting as the person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid` — the report is already shared or skipped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/weekly_reports/asks/{item_id}/offers": {
      "parameters": [
        {
          "name": "item_id",
          "in": "path",
          "required": true,
          "description": "The ask — a line in a shared report's \"Could use a hand with\" section.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Weekly Reports"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Offer to help with a teammate's ask",
        "description": "\"I can help\" on an ask in a shared report the caller may read. The\nwriter is told once, on the first offer; a client opens the Chat\nmessage to `writer` itself. Offering again is a `200` with\n`new_offer: false`. Requires `write:weekly_reports`.\n",
        "responses": {
          "201": {
            "description": "A new offer was recorded and the writer told.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeeklyReportHelpOffer"
                }
              }
            }
          },
          "200": {
            "description": "The caller had already offered; nothing new was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeeklyReportHelpOffer"
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled`, `app_forbidden`, `insufficient_permissions`, `forbidden` (the caller cannot see the report), or a service account acting as the person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no ask the caller can see has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid` — not an ask for help, the caller's own ask, or already sorted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Weekly Reports"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Take back an offer of help",
        "description": "Withdraws the caller's offer on the ask. An ask the caller offered on\nstays theirs to withdraw from even after the writer narrowed who reads\nthe report. `withdrawn` is false when there was no offer. Requires\n`write:weekly_reports`.\n",
        "responses": {
          "200": {
            "description": "The ask after the withdrawal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item_id",
                    "help_status",
                    "withdrawn"
                  ],
                  "properties": {
                    "item_id": {
                      "type": "integer",
                      "example": 812
                    },
                    "help_status": {
                      "$ref": "#/components/schemas/WeeklyReportHelpStatus"
                    },
                    "withdrawn": {
                      "type": "boolean",
                      "description": "False when the caller had no offer on this ask.",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled`, `app_forbidden`, `insufficient_permissions`, or a service account acting as the person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no ask the caller can see (or offered on) has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/new": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The New Waiver form — choices, defaults, help, and the prefill for an employee or a renewal",
        "description": "Everything the + sheet draws: each field's label, placeholder and help,\nthe Location and Jurisdiction choices, Behavior State (or, where the\norganization's break rules define more than one meal period, per-period\nelections that decide it), the default term (today to today + the\norganization's waiver length), the Valid Until rule with its \"No\nexpiration\" wording, and the 2,000-character Notes limit — in the\nclient's language. Behavior State opens with no value (`null`): it is\na choice the creator makes, never a default.\n\n`user_id` opens it for an active member the caller may create a waiver\nfor (ignored otherwise — `employee` is then null): `employee` is\nfilled, with the Location and Jurisdiction the form suggests from their\nprimary location (the web form leaves both to the creator), and\n`in_progress_waiver` names a waiver already on its way for them —\nsaving another needs `create_override`. `from_waiver_id` opens it as a\nrenewal of that waiver (\"Start a new waiver for …\"): its employee,\nlocation, jurisdiction, choice and elections, and — for one still in\nforce — the next term, starting the day after it ends.\n",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "from_waiver_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          }
        ],
        "responses": {
          "200": {
            "description": "The form.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceNewWaiverForm"
                },
                "example": {
                  "title": "New Waiver",
                  "renewal_of": null,
                  "employee": {
                    "id": 5133,
                    "name": "Omar Haddad",
                    "email": "omar.haddad@acme.com",
                    "initials": "OH",
                    "avatar_url": "https://ui-avatars.com/api/?name=OH",
                    "job_title": "Line Cook",
                    "location": {
                      "id": 41,
                      "name": "North Warehouse"
                    },
                    "jurisdiction": {
                      "id": 7,
                      "label": "California (CA)",
                      "level": "state"
                    },
                    "in_progress_waiver": null
                  },
                  "in_progress_waiver": null,
                  "create_override": null,
                  "fields": {
                    "employee": {
                      "label": "Employee",
                      "required": true,
                      "value": 5133,
                      "placeholder": "Search for an employee...",
                      "scope_note": null
                    },
                    "location": {
                      "label": "Location",
                      "required": false,
                      "value": 41,
                      "placeholder": "Select a location (optional)...",
                      "options": [
                        {
                          "id": 41,
                          "name": "North Warehouse"
                        }
                      ]
                    },
                    "behavior_state": {
                      "label": "Behavior State",
                      "required": true,
                      "shown": true,
                      "value": null,
                      "placeholder": "Choose waiving or taking…",
                      "options": [
                        {
                          "value": "waiving",
                          "label": "Waiving — skips meal breaks"
                        },
                        {
                          "value": "taking",
                          "label": "Taking — takes meal breaks"
                        }
                      ],
                      "note": null
                    },
                    "elections": {
                      "label": "Meal period elections",
                      "shown": false,
                      "options": [],
                      "periods": []
                    },
                    "jurisdiction": {
                      "label": "Jurisdiction",
                      "required": false,
                      "value": 7,
                      "placeholder": "Select jurisdiction...",
                      "options": [
                        {
                          "id": 7,
                          "label": "California (CA)",
                          "level": "state"
                        }
                      ],
                      "help": "The country, state, or city jurisdiction whose labor law this waiver is written under.",
                      "empty_note": null
                    },
                    "valid_from": {
                      "label": "Valid From",
                      "required": false,
                      "value": "2026-10-22"
                    },
                    "valid_until": {
                      "label": "Valid Until",
                      "required": false,
                      "value": "2027-10-22",
                      "min": "2026-10-22",
                      "help": "Must be today or later — …",
                      "no_expiry_label": "No expiration"
                    },
                    "notes": {
                      "label": "Notes",
                      "required": false,
                      "max_length": 2000,
                      "placeholder": "Optional notes about this waiver...",
                      "help": "The employee sees this — … Up to 2",
                      "000 characters.": null
                    }
                  },
                  "submit_label": "Create Waiver"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller may not create waivers, or the token is read-only or own-tier (`forbidden`); or the token lacks the scope (`insufficient_permissions`), or the app is not open to the caller."
          }
        }
      }
    },
    "/meal_compliance/waivers/employees": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The New Waiver employee picker — exactly the people the caller may create a waiver for",
        "description": "The web form's typeahead, so the picker never offers someone the save\nthen refuses: HR, admins and app admins search the whole directory\n(the platform's people search, on its deterministic order — no person\nrepeats or goes missing across pages); a line manager gets their own\nactive direct reports, never themself. Each person carries the\nLocation and Jurisdiction the form suggests for them (their primary\nlocation here, and its state jurisdiction — else city, else country —\namong the organization's active ones; the web form leaves both to the\ncreator) and the waiver already on its way for them. Email and job\ntitle only where the People directory shows them to the caller. A\nfixed number of queries per page.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Name (and email, where the directory shows it). Blank lists the first page."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of people (50).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string",
                      "nullable": true
                    },
                    "scope_note": {
                      "type": "string",
                      "nullable": true,
                      "description": "Who the picker lists, for a line manager: \"Only your direct reports are listed. Ask an HR or app administrator to create a waiver for anyone else.\" Null for HR and administrators, who reach everyone."
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MealComplianceWaiverEmployeeOption"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 50
                        },
                        "has_next_page": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The caller may not create waivers, or the token is read-only or own-tier (`forbidden`); or the token lacks the scope (`insufficient_permissions`), or the app is not open to the caller."
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "One waiver's details, with a permission flag per action the caller may take on it",
        "description": "**Who may open it.** The Waivers register's population: HR admins,\nbusiness admins and Meal Compliance app admins any waiver of the\norganization; a line manager their direct reports' waivers and their\nown; anyone else their own. A token holding only\n`read:own_meal_compliance` reaches the caller's own waivers whatever\ntheir role. A draft a manager discarded, and a sample waiver in a real\nperson's name, are hidden from that person here as on their list.\n\n**What they may do.** `permissions` carries one flag per action, true\nonly when the caller may take it on the waiver as it stands — the same\nanswer the web page draws its buttons from:\n\n| State | Actions offered to someone who manages the waiver |\n|---|---|\n| Draft | Send for Signature (where the organization offers e-signature and the employee can sign — see below), Activate, Discard draft |\n| Pending Signature, never sent | Send for Signature, Activate, Discard (never sent) — nobody was asked to sign it, so it is discarded, not declined |\n| Pending Signature, its request lapsed | Send for Signature, Activate, Employee declined it, Invalidate |\n| Pending Signature, out for signature | Employee declined it, Invalidate, Resend signing link / Remind to sign (a live link E-Signature would remind) |\n| Pending Signature, out for signature but the employee's link ran out unsigned | Activate, Employee declined it, Invalidate, Resend signing link / Remind to sign |\n| Active (in force or not yet in effect) | Employee withdrew it, Invalidate |\n| Expired, Invalidated (and an active waiver past its term) | Start a new waiver — while the employee is still a member and no other waiver of theirs is in force |\n\n\"Someone who manages the waiver\" is HR, an admin, an app admin, or the\nemployee's line manager — and the token must hold the wide\n`write:meal_compliance`. A former employee's waiver is never sent or\nactivated, and nobody activates their own waiver (another HR or app\nadministrator puts it on file). An employee with an email is sent a\nsigning link; one with no email is sent the waiver to sign in the app,\nafter confirming it is them with a personal PIN, a password they use\nor a code texted to their mobile — Send is withheld (`no_email`) only\nwhen they have no email AND no way to confirm it is them in the app.\nThe waiver's own employee (who does not manage it) gets\nRevoke my waiver / Withdraw before signing on a live or pending\nwaiver, with `write:own_meal_compliance` or wider, and Review & Sign\nwhile their signing link is live. Download signed\nwaiver is offered wherever E-Signature would let the caller open the\nsigned copy. A read-only token gets every flag false.\n\n`actions` lists the permitted actions where the prototype draws them —\n`primary` (the hero's button), `pinned` (the bottom bar) or `menu` (the\n⋯ menu) — each with its label and confirm sheet, and `notices` says why\nan action the page would offer is missing (a former employee, no email,\nDocument Signing switched off).\n\nThe response costs a fixed number of queries whatever the employee's\nhistory.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The waiver's id — `items[].id` from `GET /meal_compliance/waivers`."
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the words this response writes (labels, confirm sheets, notices). Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "The waiver page for the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverDetails"
                },
                "examples": {
                  "active": {
                    "summary": "An active waiver, read by HR",
                    "value": {
                      "waiver": {
                        "id": 904,
                        "status": "active",
                        "status_label": "Active",
                        "tone": "success",
                        "employee": {
                          "id": 5133,
                          "name": "Omar Haddad",
                          "email": "omar.haddad@acme.com",
                          "initials": "OH",
                          "avatar_url": "https://acme.workforce.mangoapps.com/rails/active_storage/representations/redirect/…/avatar.jpg",
                          "job_title": "Line Cook",
                          "active_member": true
                        },
                        "behavior": {
                          "state": "waiving",
                          "label": "Waiving",
                          "icon": "forward"
                        },
                        "elections": [
                          {
                            "period_ordinal": 1,
                            "label": "1st meal period",
                            "waived": true,
                            "choice_label": "Waived"
                          },
                          {
                            "period_ordinal": 2,
                            "label": "2nd meal period",
                            "waived": false,
                            "choice_label": "Taking"
                          }
                        ],
                        "elections_summary": "1st meal period: Waived · 2nd meal period: Taking",
                        "jurisdiction_name": "Washington",
                        "location": {
                          "id": 41,
                          "name": "North Warehouse"
                        },
                        "valid_from": "2026-04-01",
                        "valid_from_tag": null,
                        "valid_until": {
                          "date": "2026-10-30",
                          "label": null,
                          "days_until_expiry": 9,
                          "tag": {
                            "kind": "expiring",
                            "label": "Expires in 9 days",
                            "tone": "warning"
                          },
                          "note": null
                        },
                        "ended": null,
                        "activated_by": null,
                        "notes": null,
                        "is_sample_data": false,
                        "created_at": "2026-03-28T16:02:11Z",
                        "updated_at": "2026-04-01T09:15:40Z"
                      },
                      "esignature": {
                        "request_id": 3310,
                        "name": "Meal Break Waiver – Omar Haddad",
                        "status": "completed",
                        "status_label": "Completed",
                        "sent_at": "2026-03-28T16:05:02Z",
                        "signing_link": null,
                        "signed_copy": {
                          "esignature_request_id": 3310,
                          "download_path": "/apps/esignature/requests/3310/download"
                        }
                      },
                      "behavior_history": {
                        "limit": 20,
                        "items": [
                          {
                            "id": 77,
                            "label": "Sep 19 – Oct 3: skipped 13 of 14 days",
                            "detected_pattern": "waiving",
                            "previous_pattern": "taking",
                            "window_start": "2026-09-19",
                            "window_end": "2026-10-03",
                            "detected_at": "2026-10-03T06:00:00Z",
                            "days_analyzed": 14,
                            "days_waived": 13,
                            "days_taken": 1,
                            "action_label": null
                          }
                        ]
                      },
                      "notices": [],
                      "permissions": {
                        "can_send_for_signature": false,
                        "can_activate": false,
                        "can_invalidate": true,
                        "can_record_employee_withdrawal": true,
                        "can_discard_draft": false,
                        "can_resend_signing_link": false,
                        "can_download_signed_waiver": true,
                        "can_start_new_waiver": true,
                        "can_revoke_own_waiver": false
                      },
                      "actions": [
                        {
                          "key": "start_new_waiver",
                          "label": "Start the next waiver for Omar Haddad",
                          "icon": "plus",
                          "tone": "primary",
                          "placement": "primary",
                          "confirm": null,
                          "prefill": {
                            "user_id": 5133,
                            "from_waiver_id": 904
                          }
                        },
                        {
                          "key": "record_employee_withdrawal",
                          "label": "Employee withdrew it",
                          "icon": "rotate-left",
                          "tone": "danger",
                          "placement": "pinned",
                          "reason": "employee_revoked",
                          "confirm": {
                            "title": "Employee withdrew it?",
                            "message": "Record that Omar Haddad asked to withdraw their waiver?",
                            "confirm_label": "Confirm"
                          }
                        },
                        {
                          "key": "invalidate",
                          "label": "Invalidate",
                          "icon": "ban",
                          "tone": "danger",
                          "placement": "pinned",
                          "reason": "manual",
                          "confirm": {
                            "title": "Invalidate this waiver?",
                            "message": "Invalidate this waiver on the employer's initiative? Use this when the business is ending the waiver.",
                            "confirm_label": "Confirm"
                          }
                        },
                        {
                          "key": "download_signed_waiver",
                          "label": "Download signed waiver",
                          "icon": "file-arrow-down",
                          "tone": "primary",
                          "placement": "menu",
                          "confirm": null,
                          "esignature_request_id": 3310,
                          "download_path": "/apps/esignature/requests/3310/download"
                        }
                      ]
                    }
                  },
                  "draft": {
                    "summary": "A draft, read by the employee's line manager",
                    "value": {
                      "waiver": {
                        "id": 918,
                        "status": "draft",
                        "status_label": "Draft",
                        "tone": "info",
                        "employee": {
                          "id": 5140,
                          "name": "Priya Shah",
                          "email": null,
                          "initials": "PS",
                          "avatar_url": "https://ui-avatars.com/api/?name=PS",
                          "job_title": "Server",
                          "active_member": true
                        },
                        "behavior": {
                          "state": "taking",
                          "label": "Taking",
                          "icon": "utensils"
                        },
                        "elections": [],
                        "elections_summary": null,
                        "jurisdiction_name": "California",
                        "location": {
                          "id": 42,
                          "name": "Seattle Distribution Center"
                        },
                        "valid_from": "2026-10-21",
                        "valid_from_tag": null,
                        "valid_until": {
                          "date": "2027-04-19",
                          "label": null,
                          "days_until_expiry": 180,
                          "tag": null,
                          "note": null
                        },
                        "ended": null,
                        "activated_by": null,
                        "notes": null,
                        "is_sample_data": false,
                        "created_at": "2026-10-21T10:00:00Z",
                        "updated_at": "2026-10-21T10:00:00Z"
                      },
                      "esignature": null,
                      "behavior_history": {
                        "limit": 20,
                        "items": []
                      },
                      "notices": [
                        {
                          "key": "no_email",
                          "icon": "circle-info",
                          "message": "Priya Shah has no email address on file and no personal PIN or mobile number to confirm it's them, so they can't sign in the app. If they signed on paper, use Activate instead."
                        }
                      ],
                      "permissions": {
                        "can_send_for_signature": false,
                        "can_activate": true,
                        "can_invalidate": false,
                        "can_record_employee_withdrawal": false,
                        "can_discard_draft": true,
                        "can_resend_signing_link": false,
                        "can_download_signed_waiver": false,
                        "can_start_new_waiver": false,
                        "can_revoke_own_waiver": false
                      },
                      "actions": [
                        {
                          "key": "activate",
                          "label": "Activate",
                          "icon": "check",
                          "tone": "success",
                          "placement": "primary",
                          "confirm": {
                            "title": "Activate this waiver?",
                            "message": "Mark this waiver active and on file? Use this when the employee has signed outside the app.",
                            "confirm_label": "Activate"
                          }
                        },
                        {
                          "key": "discard_draft",
                          "label": "Discard draft",
                          "icon": "trash-can",
                          "tone": "danger",
                          "placement": "menu",
                          "reason": "manual",
                          "discard": true,
                          "confirm": {
                            "title": "Discard this draft?",
                            "message": "Discard this draft? It was never sent, so Priya Shah isn't told.",
                            "confirm_label": "Discard"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance` (`insufficient_permissions`), the app is not on for the organization (`app_disabled`), the caller may not open it (`app_access_denied`), or the waiver belongs to someone this caller may not see (`forbidden`)."
          },
          "404": {
            "description": "No such waiver in this organization, or one hidden from the caller's own list (`not_found`); or the Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/send_for_signature": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send for Signature — send the employee the waiver to sign (by email, or in the app)",
        "description": "The web page's **Send for Signature**, through the same writer\n(`MealCompliance::WaiverActions`): a draft, or a pending waiver whose\nsignature request never went out or lapsed unsigned (a new request is\nthen sent). An employee with an email is mailed a signing link; one\nwith no email is asked to sign in the app, where they confirm it is\nthem with a personal PIN, a password they use or a code texted to\ntheir mobile. Refused (422) where the organization does not offer\ne-signature (`esignature_off`: the \"Offer e-signature collection\"\nsetting; `esignature_unavailable`: the E-Signature app), the employee\nhas left (`former_member`), has no email AND no way to confirm it is\nthem in the app (`no_email`), or Document\nSigning is switched off (`signing_portal_closed`); refused (409) where\nthe waiver is already out or past being a draft (`not_sendable`) or its\nterm has ended (`term_ended`) — each with the sentence the web page shows.\n\nOffered when `permissions.can_send_for_signature` on `GET /meal_compliance/waivers/{id}`.\nRequires a caller who manages the waiver (HR, an admin, a Meal\nCompliance app admin, or the employee's line manager) and the wide\n`write:meal_compliance` scope.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "$ref": "#/components/responses/MealComplianceWaiverActionForbidden"
          },
          "404": {
            "description": "No such waiver in this organization (`not_found`)."
          },
          "409": {
            "description": "The waiver's state refuses it (`not_sendable`, `term_ended`) — a stale button or a double tap. `error.details.page` is the page as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "422": {
            "description": "The organization or the employee refuses it (`esignature_off`, `esignature_unavailable`, `former_member`, `no_email`, `signing_portal_closed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "502": {
            "description": "E-Signature refused or failed to send (`send_failed`); worth a retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/activate": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Activate — put the waiver on file without an in-app signature",
        "description": "The web page's **Activate** (\"the employee signed outside the app\"),\nthrough the same writer: a draft, or a pending waiver with no live\nsignature request out. Records who activated it (`waiver.activated_by`)\nand starts it today when it has no Valid From. Refused (409) for any\nother state (`not_activatable`) or a term that already ended\n(`term_ended`); refused (422) for a former employee (`former_member`)\nor a waiver that does not validate (`invalid`); refused (403) on the\ncaller's own waiver (`own_waiver`) — nobody attests their own\nsignature: another HR or app administrator activates it, or it is sent\nto them for signature.\n\nOffered when `permissions.can_activate`. Requires a caller who manages\nthe waiver and the wide `write:meal_compliance` scope.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "As every action (see MealComplianceWaiverActionForbidden), or the waiver is the caller's own (`own_waiver`, with `error.details.page`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "404": {
            "description": "No such waiver in this organization (`not_found`)."
          },
          "409": {
            "description": "The waiver's state refuses it (`not_activatable`, `term_ended`). `error.details.page` is the page as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "422": {
            "description": "The employee has left (`former_member`), or the waiver did not validate (`invalid`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/invalidate": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Invalidate, Employee withdrew it, or Discard",
        "description": "The web page's three ending buttons, through the same writer.\n`reason` records WHO ended the waiver — legally opposite facts on a\nrecord with no edit route, so only these two are accepted (absent is\n`manual`; anything else is 400 and nothing is written):\n\n* `manual` — **Invalidate**, on the employer's initiative.\n* `employee_revoked` — **Employee withdrew it**: the employee's own\n  notice, recorded by a manager. On a waiver not yet signed it is\n  recorded as declined (\"Employee declined it\"); it never took effect.\n* On a **draft**, or a pending waiver whose signature request never\n  went out (**Discard draft** / **Discard (never sent)**), the reason\n  is ignored: it is recorded as discarded and nobody is notified — it\n  was never sent.\n\nThe employee is notified of an\nending (not of a discard); when they could not be, the answer's `tone`\nis `warning` and the message says to tell them directly. Refused (409)\nfor an expired or ended waiver (`not_invalidatable`) and for an active\nwaiver whose term ended before the expiry sweep (`term_ended` — it\nreads Expired already).\n\nOffered when `permissions.can_invalidate`,\n`can_record_employee_withdrawal` or `can_discard_draft` (each action in\n`actions` carries the `reason` to send). Requires a caller who manages\nthe waiver and the wide `write:meal_compliance` scope.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "employee_revoked"
                    ],
                    "default": "manual"
                  },
                  "discard": {
                    "type": "boolean",
                    "default": false,
                    "description": "The discard_draft action's press (send it as the action says). A waiver sent since the page was read is then refused with `state_changed`, never cancelled with a notice to the employee."
                  }
                }
              },
              "example": {
                "reason": "employee_revoked"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "$ref": "#/components/responses/MealComplianceWaiverActionForbidden"
          },
          "404": {
            "description": "No such waiver in this organization (`not_found`)."
          },
          "400": {
            "description": "`reason` is not `manual` or `employee_revoked` (`invalid_parameter`, `details.field: reason`). Nothing was written."
          },
          "409": {
            "description": "The waiver's state refuses it (`not_invalidatable` — already ended, including by another door a moment ago; `term_ended`; `state_changed` — Discard on a waiver another door sent for signature a moment ago). `error.details.page` is the page as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "500": {
            "description": "The write failed (`invalidate_failed`); nothing changed, worth a retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/resend_signature": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resend signing link / Remind to sign — remind the employee of their live link",
        "description": "The web page's **Resend signing link** — **Remind to sign** for a\nwaiver sent to sign in the app (an employee with no email), which\nreminds them in the app and mails nothing — through the same writer and\nE-Signature's own reminder: only for a request that is out and still\nlive, on an organization that offers e-signature (`not_resendable`\notherwise — a lapsed request is Send for Signature's), never for a\nformer employee (`former_member`). A failure E-Signature reports is\n`resend_failed`.\n\nOffered when `permissions.can_resend_signing_link`. Requires a caller\nwho manages the waiver and the wide `write:meal_compliance` scope.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "$ref": "#/components/responses/MealComplianceWaiverActionForbidden"
          },
          "404": {
            "description": "No such waiver in this organization (`not_found`)."
          },
          "409": {
            "description": "No live signing link to resend (`not_resendable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "422": {
            "description": "The employee has left (`former_member`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "502": {
            "description": "E-Signature refused or failed the reminder (`resend_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/revoke": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Revoke my waiver / Withdraw before signing — the waiver's own employee",
        "description": "The employee withdraws their OWN waiver — My Breaks' **Revoke my\nwaiver** (an agreement in force or starting later) or **Withdraw before\nsigning** (one awaiting their signature, recorded as declined), through\nthe same writer as the web. They are not sent the invalidation notice\n(it would ask them to re-sign what they just withdrew); whoever must\nreplace it is told.\n\nOnly the caller's own, real waiver is found — anyone else's, a sample\nwaiver or a draft a manager discarded is 404, so an id never confirms\na waiver exists. A waiver with nothing left to revoke is 409\n(`not_revocable`). `write:own_meal_compliance` is enough. This is My\nBreaks' own door, open to anyone on their own waiver, so it does not\nfollow `permissions.can_revoke_own_waiver`, which the waiver page\nwithholds from someone who manages the waiver (they record the same\nfact with Employee withdrew it).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The token lacks `write:meal_compliance` / `write:own_meal_compliance` (`insufficient_permissions`), the app is not open to the caller, or an admin is viewing as the employee (`forbidden`)."
          },
          "404": {
            "description": "Not the caller's own real waiver (`not_found`)."
          },
          "409": {
            "description": "Nothing left to revoke (`not_revocable` — ended, expired, or ended by another door a moment ago).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "500": {
            "description": "The write failed (`revoke_failed`); nothing changed, worth a retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/decline": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Decline to Sign — the waiver's own employee refuses its signing request, with an optional reason",
        "description": "The **Decline** beside Review & Sign, in the app (the prototype's\n\"Decline to Sign\" sheet): the employee refuses their pending waiver's\nsigning request, through E-Signature's own writer — the signing page's\nDecline — so the signer row, the request, the audit trail the signing\ncertificate prints and the sender's email and bell are written the\nsame way. The waiver then ends as declined by the employee (it never\ntook effect), and its E-Signature status reads Declined with the\nreason.\n\nOffered exactly where Review & Sign is — My Breaks'\n`pending_waiver.can_decline` / `decline` sheet, and the waiver page's\n`permissions.can_decline_to_sign`: the caller's own, real waiver,\nawaiting their signature on a live request (sent, not yet answered,\nlink unexpired, Document Signing on). Where it is offered, Withdraw\nbefore signing is not (both end the waiver as declined by the\nemployee). The caller is the signed-in member the signer row belongs\nto, so the signing link and its verification code are not asked for —\ndeliberately: the code proves who holds a link, and this caller is\nalready authenticated as that person. An admin's View-as never\ndeclines for the person (403). `write:own_meal_compliance` is enough.\nA decline of theirs already on record (a double tap, a lost answer)\nanswers the same receipt.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1000,
                    "description": "Optional. Shown to the sender and to whoever has authority over the request."
                  }
                }
              },
              "example": {
                "reason": "I'd rather keep my standard meal breaks for now."
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "400": {
            "description": "`reason` is not text (`invalid_parameter`, `details.field: reason`). Nothing was written."
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The token lacks `write:meal_compliance` / `write:own_meal_compliance` (`insufficient_permissions`), the app is not open to the caller, or an admin is viewing as the employee (`forbidden`)."
          },
          "404": {
            "description": "Not the caller's own real waiver (`not_found`)."
          },
          "409": {
            "description": "Not waiting for the caller's signature (`not_declinable` — never sent, already signed, the request closed, the link expired or Document Signing is off; My Breaks' `signing.note` says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "422": {
            "description": "The reason is over 1,000 characters (`reason_too_long`). Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "500": {
            "description": "The write failed (`decline_failed`); nothing changed, worth a retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/waivers/{id}/sign": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Sign — the app's own Sign Document on its Review & Sign sheet",
        "description": "The waiver's own employee signs their pending waiver with the signature\ntheir app captured, the date and their consent, through E-Signature's\nown writer — the signing page's Sign — so the field values, the consent\nin the audit trail, the executed copy, the certificate and the copies\nmailed are the same, and the audit row records that the signature came\nfrom the signed-in app (`signed_via`). Once the executed copy is made the\nwaiver goes on file; the answer is the waiver page as it is then. When it\ndid not go on file — the executed copy could not be produced, or the\nwaiver could not be put in force — the answer is still 200, with `tone:\nwarning` and a `message` saying so. Signing again after a signature\nthat went through (a double tap, a lost answer) answers the same receipt.\n\nThe sheet shows the waiver read-only in a web view (My Breaks'\n`signing.web_view_url`) and draws the signature pad, consent, Sign\nDocument and Decline itself. Offered where `signing.can_sign_in_app` /\n`permissions.can_sign_in_app` is true: the caller's own real waiver,\nawaiting their signature on a live request, before its term ends. The caller is the\nsigned-in member the signer row belongs to, so the signing link and its\nverification code are not asked for — deliberately, as for Decline. An\nadmin's View-as never signs for the person (403).\n`write:own_meal_compliance` is enough.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MealComplianceWaiverId"
          },
          {
            "$ref": "#/components/parameters/MealComplianceLocale"
          },
          {
            "$ref": "#/components/parameters/MealComplianceIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "signature",
                  "consent"
                ],
                "properties": {
                  "signature": {
                    "type": "object",
                    "required": [
                      "type",
                      "value"
                    ],
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "typed",
                          "drawn"
                        ]
                      },
                      "value": {
                        "type": "string",
                        "description": "typed: the name (up to 200 characters). drawn: a PNG as a data URL (`data:image/png;base64,…`, up to 256 KB)."
                      },
                      "font": {
                        "type": "string",
                        "enum": [
                          "cursive",
                          "elegant",
                          "formal"
                        ],
                        "description": "typed only; cursive when absent."
                      }
                    }
                  },
                  "date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "Their own calendar day (YYYY-MM-DD); their today where they are when absent."
                  },
                  "consent": {
                    "type": "boolean",
                    "description": "true — \"I agree that my electronic signature is legally binding\". Recorded in the audit trail."
                  }
                }
              },
              "example": {
                "signature": {
                  "type": "typed",
                  "value": "Marcus Bell",
                  "font": "cursive"
                },
                "date": "2026-10-23",
                "consent": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/MealComplianceWaiverActionDone"
          },
          "400": {
            "description": "`signature` is not an object, or `date` is not text (`invalid_parameter`, `details.field`). Nothing was written."
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "The token lacks `write:meal_compliance` / `write:own_meal_compliance` (`insufficient_permissions`), the app is not open to the caller, or an admin is viewing as the employee (`forbidden`)."
          },
          "404": {
            "description": "Not the caller's own real waiver (`not_found`)."
          },
          "409": {
            "description": "Not waiting for the caller's signature (`not_signable` — never sent, declined, the request closed, the link expired, Document Signing is off, or the term has ended).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "422": {
            "description": "No consent (`consent_required`), a signature the signed copy cannot render (`signature_invalid`), a date that is not one (`date_invalid`), a drawn signature over 256 KB (`signature_too_large`), or a required field the app cannot fill (`fields_missing`). Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          },
          "500": {
            "description": "A write was refused (`sign_failed`); nothing was saved, worth a retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceWaiverActionError"
                }
              }
            }
          }
        }
      }
    },
    "/meal_compliance/my_breaks": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "My Breaks — the caller's own waiver status, break pattern and waiver history",
        "description": "Every role gets the same shape: an employee, a line manager and an HR\nadmin each see only **their own** waivers and break pattern here (the team\nviews are `/meal_compliance/dashboard` and `/meal_compliance/waivers`).\n\nSections:\n* `pending_waiver` — a waiver awaiting the caller's signature (the web\n  banner). `signing.can_sign` with `signing.sign_url` is the Review & Sign\n  button — the caller's own live signing link; otherwise `signing.note`\n  says why not (never sent, link expired, request voided, …) and\n  `signing.state` names the state for a client that branches.\n* `current_waiver` — the agreement on the \"Current waiver status\" card: in\n  force today, else one starting later (`not_yet_in_effect`) or one whose\n  term has passed (`expired`). Null when there is none, with\n  `no_current_waiver` carrying the empty state.\n* `break_pattern` — the newest 10 detected break-pattern windows (\"Oct 1 –\n  Oct 15: skipped 11 of 14 days\"), observations only, with the uncapped\n  `total` and a `truncation_note` when the list is cut.\n* `waiver_history` — every other waiver (ended, expired), newest first.\n* `drafts` — waivers the caller's manager is still preparing: counted, not\n  listed — nothing is in effect and there is nothing to sign.\n\n`signing.web_view_url` is the waiver as a read-only document for the app's\nReview & Sign sheet; `signing.can_sign_in_app` with its `sign` sheet is the\napp's own Sign Document (`POST /meal_compliance/waivers/{id}/sign`).\n`can_decline` with its `decline` sheet is the prototype's Decline beside\nReview & Sign — `POST /meal_compliance/waivers/{id}/decline`, offered\nexactly where `signing.can_sign` is (not on a read-only token, nor\nunder an admin's View-as). Where it is offered, `can_withdraw` is false:\nDecline replaces Withdraw before signing, which stays for a waiver\nnever sent or whose link lapsed.\n\n`can_withdraw` / `can_revoke` and their `*_confirm` sentences are the web's\nWithdraw before signing / Revoke my waiver controls (the employee ends\ntheir own agreement); they are false on a read-only token, as on\nWaiver Details. A sample waiver is never revocable; a real person\nnever sees sample rows here. `signed_copy` appears only where E-Signature\nwould let the caller download it (`GET /esignature/{id}` answers a\ntime-limited `signed_document_url`).\n\nLabels and the page's sentences are written in the language the client\nasks for (`locale`, then Accept-Language, then the caller's own); the\nbreak-pattern row `label` / `action_label` and a past waiver's `ended_reason`\nare still English, as on the web page. The response costs a fixed number\nof queries whatever the caller's history.\n",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the sentences and labels this response writes. Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's My Breaks record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceMyBreaks"
                },
                "example": {
                  "title": "My Meal Breaks",
                  "intro": "Your meal-break waiver status and recent break history — what's on file about your breaks, visible to you.",
                  "pending_waiver": {
                    "id": 50,
                    "status": "pending_signature",
                    "status_label": "Pending Signature",
                    "tone": "warning",
                    "behavior_state": "waiving",
                    "elections": [],
                    "elections_summary": "Waiving meal breaks",
                    "jurisdiction_name": "Oregon",
                    "location": {
                      "id": 31,
                      "name": "Downtown Store"
                    },
                    "valid_from": "2026-10-21",
                    "valid_until": {
                      "date": "2027-10-21",
                      "label": null,
                      "days_until_expiry": 365,
                      "tag": null
                    },
                    "notes": null,
                    "is_sample_data": false,
                    "updated_at": "2026-10-21T09:12:44Z",
                    "signed_copy": null,
                    "details_path": "/apps/meal-compliance/waivers/50",
                    "heading": "A meal-break waiver is waiting for your signature.",
                    "terms": "Signing records your choice to waive meal breaks in Oregon. Review it before signing — this is your decision, not a requirement.",
                    "signing": {
                      "state": "ready",
                      "can_sign": true,
                      "sign_url": "https://officechat.workforce.mangoapps.com/sign/9f3c…",
                      "web_view_url": "https://officechat.workforce.mangoapps.com/m/apps/meal-compliance/88/document",
                      "can_sign_in_app": true,
                      "sign": {
                        "label": "Sign Document",
                        "signature_label": "Signature",
                        "signature_types": [
                          "typed",
                          "drawn"
                        ],
                        "signature_fonts": [
                          "cursive",
                          "elegant",
                          "formal"
                        ],
                        "date_label": "Date",
                        "date": "2026-10-23",
                        "consent_label": "I have read the electronic records disclosure and agree to sign electronically. My electronic signature is legally binding.",
                        "disclosure": {
                          "title": "Electronic records and signatures disclosure",
                          "text": "Harbor & Pine is asking you to sign this document electronically…",
                          "paragraphs": [
                            "Harbor & Pine is asking you to sign this document electronically…"
                          ],
                          "version": "2026-10-24"
                        },
                        "hint": "Sign, check the date, then confirm that your electronic signature is legally binding."
                      },
                      "note": null,
                      "link_expires_at": "2026-11-20T09:12:44Z"
                    },
                    "can_decline": true,
                    "decline": {
                      "label": "Decline",
                      "title": "Decline to Sign",
                      "message": "Are you sure you want to decline?",
                      "confirm_label": "Decline",
                      "reason_field": {
                        "label": "Reason (optional)",
                        "required": false,
                        "max_length": 1000
                      }
                    },
                    "can_withdraw": false,
                    "withdraw_confirm": null,
                    "sample_note": null
                  },
                  "pending_count": 1,
                  "current_waiver": {
                    "id": 31,
                    "status": "active",
                    "status_label": "Active",
                    "tone": "success",
                    "behavior_state": "waiving",
                    "elections": [
                      {
                        "period_ordinal": 1,
                        "label": "1st meal period",
                        "waived": true,
                        "choice_label": "Waived"
                      }
                    ],
                    "elections_summary": "1st meal period: Waived",
                    "jurisdiction_name": "California",
                    "location": {
                      "id": 31,
                      "name": "Downtown Store"
                    },
                    "valid_from": "2025-10-30",
                    "valid_until": {
                      "date": "2026-10-30",
                      "label": null,
                      "days_until_expiry": 9,
                      "tag": {
                        "kind": "expiring",
                        "label": "Expires in 9 days",
                        "tone": "warning"
                      }
                    },
                    "notes": "Agreed at the store meeting.",
                    "is_sample_data": false,
                    "updated_at": "2026-09-02T16:20:11Z",
                    "signed_copy": {
                      "esignature_request_id": 812,
                      "download_path": "/apps/esignature/requests/812/download"
                    },
                    "details_path": "/apps/meal-compliance/waivers/31",
                    "choice_label": "Waiving meal breaks",
                    "in_force": true,
                    "start_sentence": "In effect since Oct 30, 2025.",
                    "end_sentence": "Valid until Oct 30, 2026.",
                    "expiry": {
                      "expiring_soon": true,
                      "label": "Expires in 9 days",
                      "renewed": false,
                      "renewed_note": null
                    },
                    "can_revoke": true,
                    "revoke_explainer": "Changed your mind? You can revoke a meal-break waiver at any time. It goes on file as withdrawn by you, so anyone who reviews it sees the decision was yours.",
                    "revoke_confirm": "Revoke your meal-break waiver? You'll go back to taking your meal breaks as scheduled.",
                    "sample_note": null
                  },
                  "no_current_waiver": null,
                  "break_pattern": {
                    "total": 12,
                    "limit": 10,
                    "truncation_note": "Showing your 10 most recent entries of 12.",
                    "empty_state": null,
                    "items": [
                      {
                        "id": 902,
                        "label": "Oct 1 – Oct 15: skipped 11 of 14 days",
                        "detected_pattern": "waiving",
                        "previous_pattern": "taking",
                        "window_start": "2026-10-01",
                        "window_end": "2026-10-15",
                        "detected_at": "2026-10-15T06:00:00Z",
                        "days_analyzed": 14,
                        "days_waived": 11,
                        "days_taken": 3,
                        "action_label": null
                      }
                    ]
                  },
                  "waiver_history": {
                    "total": 1,
                    "items": [
                      {
                        "id": 22,
                        "status": "invalidated",
                        "status_label": "Withdrawn",
                        "tone": "neutral",
                        "behavior_state": "waiving",
                        "elections": [],
                        "elections_summary": "Waiving meal breaks",
                        "jurisdiction_name": "California",
                        "location": {
                          "id": 31,
                          "name": "Downtown Store"
                        },
                        "valid_from": "2024-10-30",
                        "valid_until": {
                          "date": "2025-10-29",
                          "label": null,
                          "days_until_expiry": -357,
                          "tag": null
                        },
                        "notes": null,
                        "is_sample_data": false,
                        "updated_at": "2025-08-11T10:02:00Z",
                        "signed_copy": null,
                        "details_path": "/apps/meal-compliance/waivers/22",
                        "choice_label": "Waiving",
                        "valid_label": "Oct 2024 – Oct 2025",
                        "updated_on": "Aug 11, 2025",
                        "ended_reason": "You withdrew your agreement to waive meal breaks."
                      }
                    ]
                  },
                  "drafts": {
                    "count": 1,
                    "note": "Your manager is preparing 1 waiver that hasn’t been sent yet. It isn’t in effect, and there is nothing for you to sign."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance` (`insufficient_permissions`), the app is not on for the organization (`app_disabled`), or the caller may not open it (`app_access_denied`)."
          },
          "404": {
            "description": "The Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      }
    },
    "/meal_compliance/break_compliance": {
      "get": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The Break Compliance report (tiles, rules in force, findings)",
        "description": "Meal and rest break findings from recorded time — what the break\nengine recorded when each shift was clocked out — for the whole\norganization. Findings name individual employees and their\ntimekeeping, so only a business admin, a Meal Compliance app admin or\nan HR admin may read them, as on the web.\n\n**Tiles** count the FILTERED set, before the page limit:\n`findings`, `unresolved` (left out while a `status` is selected —\nunder Resolved it would be a structural 0, under Unresolved a copy of\nFindings), `employees_affected`, and `by_period` whose three `parts`\n(Meal / Rest / Unbroken work) add up to Findings. A tile or part with\na `filter` opens the list narrowed that way; one with none (a zero)\ndoes not.\n\n**Filters** (`filters`) are the All / Unresolved / Resolved chips,\ncounted under every OTHER filter, so a chip never advertises rows the\nlist hides; `selected` marks the current one.\n\n**Rules in force** lists each break rule resolving for the\norganization's active locations, with the locations it covers.\n`state` is `listed`, `none` (no location's jurisdictions resolve a\nbreak rule, so nothing is being evaluated) or `unavailable` (the rules\ncould not be worked out just now — `total` is then null, and an empty\nreport means nothing either way).\n\n**Empty state.** When no finding matches, `empty_state.kind` says why:\n`rules_unavailable`, `no_rules`, `filtered`, or `none_recorded` — and\nnone of them is an all-clear.\n\nA filter value that is not recognised is not applied: the list shows\nwhat it would without it, and `meta.filter_ignored` /\n`meta.ignored_filters` say which. An employee id naming nobody in the\norganization IS applied (nothing matches) and sets\n`applied_filters.employee_unknown`.\n\nThe response costs a fixed number of queries whatever the number of\nfindings, people or locations.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "open",
                "unresolved",
                "resolved"
              ],
              "default": "all"
            },
            "description": "`open` (alias `unresolved`) — not yet resolved; `resolved` — resolved."
          },
          {
            "name": "period_kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "meal",
                "rest",
                "any"
              ]
            },
            "description": "`any` is Unbroken work — findings that belong to no single meal or rest period."
          },
          {
            "name": "event_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "too_early",
                "too_late",
                "missing",
                "short",
                "second_meal_missing",
                "unbroken_interval_exceeded"
              ]
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "One employee's findings (a person in this organization)."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Work date on or after (YYYY-MM-DD)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Work date on or before (YYYY-MM-DD)."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Clamped to the last page, so an out-of-range page returns the last page, never an empty list."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "es"
            },
            "description": "The language for the words this response writes (labels, captions, notes). Omitted, the Accept-Language header decides, then the caller's own language."
          }
        ],
        "responses": {
          "200": {
            "description": "The report. Also sets X-Total-Count, X-Total-Pages, X-Current-Page and X-Per-Page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealComplianceBreakComplianceReport"
                },
                "example": {
                  "applied_filters": {
                    "status": "all",
                    "event_type": null,
                    "period_kind": null,
                    "from": null,
                    "to": null,
                    "employee": null,
                    "employee_unknown": false,
                    "active": false
                  },
                  "enforcement": {
                    "mode": "observe",
                    "label": "Observing only",
                    "description": "Each shift is checked when it is clocked out and what it finds is listed below. This mode reaches nobody: no employee is prompted, no assignment is blocked, and no pay is adjusted.",
                    "icon": "eye",
                    "tone": "info",
                    "can_change": true
                  },
                  "tiles": [
                    {
                      "key": "findings",
                      "label": "Findings",
                      "value": 7,
                      "caption": "All findings on file",
                      "icon": "clipboard-list",
                      "tone": "primary"
                    },
                    {
                      "key": "unresolved",
                      "label": "Unresolved",
                      "value": 5,
                      "caption": "Still open on this backlog",
                      "icon": "circle-exclamation",
                      "tone": "danger",
                      "filter": {
                        "status": "open"
                      }
                    },
                    {
                      "key": "employees_affected",
                      "label": "Employees affected",
                      "value": 6,
                      "caption": "With at least one finding",
                      "icon": "users",
                      "tone": "info"
                    },
                    {
                      "key": "by_period",
                      "label": "Meal / Rest / Unbroken work",
                      "value": 7,
                      "caption": "The three add up to Findings",
                      "hint": "Unbroken work counts findings that belong to no single meal or rest period — worked longer than the maximum unbroken stretch.",
                      "icon": "utensils",
                      "tone": "warning",
                      "parts": [
                        {
                          "period_kind": "meal",
                          "label": "Meal",
                          "count": 4,
                          "selected": false,
                          "filter": {
                            "period_kind": "meal"
                          }
                        },
                        {
                          "period_kind": "rest",
                          "label": "Rest",
                          "count": 2,
                          "selected": false,
                          "filter": {
                            "period_kind": "rest"
                          }
                        },
                        {
                          "period_kind": "any",
                          "label": "Unbroken work",
                          "count": 1,
                          "selected": false,
                          "filter": {
                            "period_kind": "any"
                          }
                        }
                      ]
                    }
                  ],
                  "rules_in_force": {
                    "state": "listed",
                    "total": 2,
                    "note": null,
                    "items": [
                      {
                        "rule_id": 88,
                        "summary": "Breaks: meal period after 5h; second meal period after 10h; 10-min rest every 4h",
                        "location_count": 2,
                        "locations": [
                          {
                            "id": 31,
                            "name": "North Warehouse"
                          },
                          {
                            "id": 32,
                            "name": "South Bay Store"
                          }
                        ],
                        "more_locations_count": 0
                      }
                    ],
                    "can_manage_rules": true
                  },
                  "filters": [
                    {
                      "key": "all",
                      "label": "All",
                      "count": 7,
                      "selected": true
                    },
                    {
                      "key": "open",
                      "label": "Unresolved",
                      "count": 5,
                      "selected": false
                    },
                    {
                      "key": "resolved",
                      "label": "Resolved",
                      "count": 2,
                      "selected": false
                    }
                  ],
                  "options": {
                    "event_types": [
                      {
                        "key": "missing",
                        "label": "Missing"
                      },
                      {
                        "key": "too_late",
                        "label": "Started too late"
                      }
                    ],
                    "period_kinds": [
                      {
                        "key": "meal",
                        "label": "Meal"
                      },
                      {
                        "key": "rest",
                        "label": "Rest"
                      },
                      {
                        "key": "any",
                        "label": "Unbroken work"
                      }
                    ]
                  },
                  "items": [
                    {
                      "id": 5501,
                      "work_date": "2026-10-02",
                      "employee": {
                        "id": 5120,
                        "name": "Jamal Reed",
                        "initials": "JR",
                        "avatar_url": "https://ui-avatars.com/api/?name=Jamal%20Reed"
                      },
                      "finding": {
                        "type": "missing",
                        "label": "Missing",
                        "tone": "danger"
                      },
                      "period": {
                        "kind": "meal",
                        "ordinal": 1,
                        "label": "1st meal period"
                      },
                      "description": "1st meal period was not taken",
                      "severity": "violation",
                      "mode": {
                        "key": "observe",
                        "label": "Observe",
                        "tone": "neutral"
                      },
                      "status": "open",
                      "status_label": "Unresolved",
                      "resolution": null,
                      "attendance_record_id": 99812,
                      "detected_at": "2026-10-02T18:04:11Z"
                    }
                  ],
                  "empty_state": null,
                  "meta": {
                    "total_count": 7,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 25,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "permissions": {
                    "can_resolve": true,
                    "can_change_enforcement_mode": true,
                    "can_manage_rules": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks `read:meal_compliance`, or holds only the own-tier `read:own_meal_compliance` (`insufficient_permissions`); the app is not on for the organization (`app_disabled`); the caller may not open it (`app_access_denied`); or the caller is not a business admin, Meal Compliance app admin or HR admin (`forbidden`)."
          },
          "404": {
            "description": "The Meal Compliance app is not installed (`app_not_found`)."
          }
        }
      }
    },
    "/meal_compliance/break_compliance/{id}/resolve": {
      "post": {
        "tags": [
          "Meal Compliance"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve a break finding with a resolution note",
        "description": "The prototype's \"Resolve this finding\" sheet: an optional\n**Resolution note** — the reason of record if the finding is ever\nquestioned in a wage-and-hour review — then **Mark resolved**. Records\nwho resolved the finding, when, and the note, and answers with the\nfinding as the report's `items[]` draws it, so the client swaps the\ncard in place (its `status` becomes `resolved` and `resolution` is\nfilled). Show `message` as the confirmation toast.\n\n**Resolving is permanent.** A resolved finding cannot be reopened, and\nre-running the evaluator does not restore it. Resolving an\nalready-resolved finding answers `409 already_resolved` with the finding\nas it was first resolved — its adjudicator, time and note are never\nreplaced — so the client can redraw the card from `error.details.finding`.\nTwo people resolving the same finding at once cannot both win: the\nsecond is answered 409.\n\n**Resolving records a decision; it does not change pay.** To change\nwhat the day pays, correct the break on the employee's punch in Time &\nAttendance.\n\nOnly the people who may read the report may resolve its findings: a\nbusiness admin, a Meal Compliance app admin or an HR admin — the web\npage's gate. A line manager or an employee is answered `403 forbidden`.\nShow the Resolve action only while the report's\n`permissions.can_resolve` is true and the card's `status` is `open`.\n\nThe response costs a fixed number of queries.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The finding's `id` (a report `items[].id`)."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_note": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional. Stored with surrounding whitespace removed; blank or omitted stores no note. A value that is not a string (a list, an object, a number) is refused with 400 rather than dropped."
                  }
                }
              },
              "example": {
                "resolution_note": "Break was taken but not clocked; break added to the punch."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The finding was resolved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "finding"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "The confirmation, in the caller's language (\"Finding marked resolved.\")."
                    },
                    "finding": {
                      "$ref": "#/components/schemas/MealComplianceBreakFinding"
                    }
                  }
                },
                "example": {
                  "message": "Finding marked resolved.",
                  "finding": {
                    "id": 3121,
                    "work_date": "2026-10-06",
                    "employee": {
                      "id": 5133,
                      "name": "Omar Haddad",
                      "initials": "OH",
                      "avatar_url": "https://ui-avatars.com/api/?name=Omar%20Haddad"
                    },
                    "finding": {
                      "type": "missing",
                      "label": "Missing",
                      "tone": "danger"
                    },
                    "period": {
                      "kind": "meal",
                      "ordinal": 1,
                      "label": "1st meal period"
                    },
                    "description": "1st meal period was not taken",
                    "severity": "violation",
                    "mode": {
                      "key": "observe",
                      "label": "Observe",
                      "tone": "neutral"
                    },
                    "status": "resolved",
                    "status_label": "Resolved",
                    "resolution": {
                      "resolved_at": "2026-10-21T15:42:10Z",
                      "resolved_by": {
                        "id": 1,
                        "name": "Anup Kumar"
                      },
                      "note": "Break was taken but not clocked; break added to the punch."
                    },
                    "attendance_record_id": 88412,
                    "detected_at": "2026-10-06T23:05:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`resolution_note` was sent but is not a string (`invalid_parameter`, `error.details.field: resolution_note`). Nothing was written.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "invalid_parameter",
                    "message": "resolution_note must be a string",
                    "details": {
                      "field": "resolution_note"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "402": {
            "description": "The organization's monthly API allowance is spent (`api_credits_exhausted`)."
          },
          "403": {
            "description": "The token lacks the wide `write:meal_compliance` — a read-only or own-tier token (`insufficient_permissions`); the caller is not a business admin, Meal Compliance app admin or HR admin (`forbidden`, \"Compliance reports are available to HR and administrators.\"); the app is not on for the organization (`app_disabled`); or the caller may not open it (`app_access_denied`). Nothing was written."
          },
          "404": {
            "description": "No such finding in the caller's organization (`not_found`, \"That finding no longer exists.\") — a finding of another organization is never reached — or the Meal Compliance app is not installed (`app_not_found`)."
          },
          "409": {
            "description": "The finding was already resolved (`already_resolved`). Its first adjudication is kept; `error.details.finding` is the finding as it stands, to redraw the card.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "already_resolved",
                    "message": "This finding was already resolved, so it cannot be resolved again.",
                    "details": {
                      "finding": {
                        "id": 3121,
                        "status": "resolved",
                        "status_label": "Resolved",
                        "resolution": {
                          "resolved_at": "2026-10-21T15:40:02Z",
                          "resolved_by": {
                            "id": 77,
                            "name": "Priya Shah"
                          },
                          "note": "Corrected on the punch."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The finding could not be saved (`validation_error`, with the field errors)."
          }
        }
      }
    },
    "/frontline_execution/coverage": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The membership-rooted Coverage rollup (nodes / campaign lens)",
        "description": "Returns the rollup rooted at wherever the caller is a MEMBER, in one of two\nlenses. With no `node`, it opens on the children of the caller's topmost\nmembership (a corporate member on their regions, a region member on their\ndistricts); an admin with no membership opens on the whole estate. `node`\ndrills into a child location (a region → its districts, a district → the\ncampaigns running there); `view` picks the lens; `status` filters the\ncampaign 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). The drill is LEVEL-aware: each step shows only the shallowest location tier directly beneath the node (its districts, say), skipping a store or a level-less location someone mis-parented directly under it. Inactive locations are never shown. Omitted, the response opens at the top of the caller's membership. 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": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "One campaign's coverage — flat by-location list plus optional hierarchy drill",
        "description": "Returns the campaign header, an SQL-counted `summary`, the worst-first\n`by_location` slice (each row carrying a coverage-health `status`), and\nthe optional `?node=` hierarchy rollup. Everything is bounded to the\ncaller's location span.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign to report on."
          },
          {
            "name": "node",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A location id to open the hierarchy under. Absent, `hierarchy.nodes` is the top of the caller's span; present, it is the children of this node with each subtree rolled up."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "How many `by_location` rows to return (worst-first). Defaults to 25, clamped to 1–100. `has_more_locations` says whether the cut bit."
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign's coverage for this caller's span.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCoverage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `read: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/{id}/coverage/remind": {
      "post": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remind everyone still outstanding on one campaign (span-bounded)",
        "description": "Queues a reminder to every person still holding outstanding work on\nthis campaign inside the caller's span, and answers at once with who\nthe queued pass is reminding — counted before anything is sent. The\nreminders go out in the background, under the same 200-reminder cap\nand 24-hours-per-person throttle as the web Coverage page's Remind\nbutton.\n\n- `reminded` — the people the queued pass is reminding (within the cap).\n- `skipped` — the people reminded in the last 24 hours; they are not\n  reminded again.\n- `remaining` — the people past the cap; run Remind again to reach them.\n- `queued` — whether a pass was queued at all (false when everyone\n  outstanding was reminded in the last 24 hours, or nobody is\n  outstanding).\n\nWhen nobody is outstanding the answer is a clean 200 with every count\n0, `queued: false` and \"Nobody has work outstanding on this campaign.\"\n\nWhen the caller's whole span holds fewer people than the company's\nsmallest report group on a post a works council covers, no count is\ngiven: `reminded`, `skipped` and `remaining` are null, and\n`people_withheld`, `more_outstanding` and a count-free `message` are\nreturned instead.\n\n**Required Scopes:** `write:execution`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:execution\").\nA browser session is asked no scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign to remind on."
          }
        ],
        "responses": {
          "200": {
            "description": "The pass was queued (or there was nothing to queue — nobody outstanding, or everyone outstanding reminded in the last 24 hours).",
            "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": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List campaigns with a per-campaign coverage breakdown",
        "description": "Returns the tenant's campaigns, newest first, each with a `coverage`\nobject rolling up its obligations by status within the caller's location\nspan. The breakdown drives the Coverage screen's \"By campaign\" cards:\n`done`, `in_progress` (with its `awaiting_review` subset), `not_started`,\n`missed`, `blocked` and `unassigned` (plus the `total`/`percent` a\nclient can render a bar from) — the same values the Coverage\nendpoint's campaign lens reports for each campaign. A post's `percent`\nis its PEOPLE (`unit: people`, with the `people` pair it is drawn\nfrom), as on the web Campaigns and Coverage lists.\n\nPaginated: send `page` (and optionally `limit`) and read `meta` to page.\nA caller that sends neither gets page 1 at the default size, exactly as\nbefore pagination was added. An optional `status` filter narrows the list\nto one campaign lifecycle state; a non-scalar or unknown value is ignored\nrather than erroring, and `meta.status_filter_ignored` says so.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "scheduled",
                "active",
                "closed"
              ]
            },
            "description": "Filter to one campaign lifecycle status. If omitted the whole list is returned. A value outside the enum (or an array) is ignored and `meta.status_filter_ignored` is set."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "1-based page number (default 1)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Page size (default and max are server-clamped)."
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign list for this page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineExecutionCampaignsResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks `read:execution` (`insufficient_permissions`), or the caller is not in the campaign-author tier (`forbidden`)."
          }
        }
      }
    },
    "/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}": {
      "get": {
        "tags": [
          "Frontline Execution"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Campaign detail — everything the web campaign page shows",
        "description": "One campaign's full detail. Lifecycle decides which panels are present:\n`coverage` / `launch_gaps` / `required_training` exist only after launch;\n`approval` / `readiness` / `writing_check` / `targets` / `audience_load` /\n`release` only before. `viewer.can` and `release` are the same predicates\nthe web header and overflow menu draw from, so a client that honours them\nnever offers a control the server refuses. There is no launch or submit\nendpoint on this API yet — `release.action` says which control the web\noffers and whether it is live.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The campaign."
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetail"
                },
                "example": {
                  "campaign": {
                    "id": 734,
                    "name": "Winter coat planogram",
                    "status": "draft",
                    "status_label": "Draft",
                    "priority": "important",
                    "priority_label": "Important",
                    "work_type": "task",
                    "work_type_label": "Task",
                    "starts_on": "2026-10-02",
                    "ends_on": "2026-10-09",
                    "releases_on": null,
                    "launched": false,
                    "launched_at": null,
                    "closed_at": null
                  },
                  "viewer": {
                    "author_tier": true,
                    "edit_mode": "draft",
                    "can": {
                      "view_coverage": false,
                      "view_history": false,
                      "edit": true,
                      "edit_translations": true,
                      "clone": true,
                      "close": false,
                      "delete": true,
                      "ask_question": true
                    }
                  },
                  "release": {
                    "state": "outside_span",
                    "action": "launch",
                    "enabled": false,
                    "blocked_reason": "This campaign targets 1 location outside the locations you manage. Ask an admin to launch it, or submit it for publication approval.",
                    "requires_reason": false,
                    "reason_min_length": null,
                    "locations_outside_span": 1
                  },
                  "approval": {
                    "required": false,
                    "state": null
                  },
                  "coverage": null,
                  "launch_gaps": null,
                  "required_training": null,
                  "readiness": {
                    "ready": false,
                    "warnings": [
                      {
                        "code": "past_dated",
                        "message": "The start date is in the past."
                      }
                    ],
                    "availability": {
                      "applicable": false,
                      "checked": false,
                      "failed": false
                    }
                  },
                  "writing_check": {
                    "measured_against": "clarity_baseline",
                    "result": null
                  },
                  "targets": {
                    "mode": "locations",
                    "mode_label": "2 specific locations",
                    "count": 2,
                    "not_targeted_count": null,
                    "concerns": false,
                    "locations": [
                      {
                        "id": 11,
                        "name": "Store A"
                      }
                    ],
                    "locations_overflow": 1,
                    "dropped_count": 0,
                    "dropped": [],
                    "dropped_overflow": 0,
                    "unstaffed_count": 0,
                    "unstaffed": [],
                    "unstaffed_overflow": 0
                  },
                  "audience_load": {
                    "tier": "has_room",
                    "overloaded": false,
                    "window_days": 7,
                    "headcount": 14,
                    "per_person": 1.4,
                    "total_open_items": 20,
                    "capacity": 490,
                    "counts": {
                      "campaign_items": 6,
                      "tasks": 9,
                      "must_reads": 3,
                      "training_due": 2
                    },
                    "scheduled_minutes": 5880,
                    "estimated_minutes_total": 240,
                    "unestimated_items": 4,
                    "degraded_sources": [],
                    "recommendation": "This group has room for this campaign.",
                    "computed_at": "2026-09-29T10:15:00Z"
                  },
                  "brief": {
                    "instructions_html": "<p>Reset the <strong>winter</strong> endcap.</p>",
                    "instructions_text": "Reset the winter endcap.",
                    "checklist": [
                      "Strip the old set",
                      "Photograph it"
                    ],
                    "documents": [
                      {
                        "id": 91,
                        "filename": "planogram.pdf",
                        "byte_size": 48213,
                        "content_type": "application/pdf",
                        "url": "https://officechat.workforce.mangoapps.com/rails/active_storage/…"
                      }
                    ]
                  },
                  "setup": {
                    "work": {
                      "type": "task",
                      "label": "Task",
                      "description": "Something to do, optionally a checklist. Lands in the holder's tasks.",
                      "template_label": "Endcap reset",
                      "inspection": null
                    },
                    "assignment": {
                      "strategy": "claim_pool",
                      "label": "Anyone at the store can claim it",
                      "timing": "on_pickup",
                      "timing_label": "Assigned when someone picks it up on shift."
                    },
                    "targeting_label": "2 specific locations",
                    "owning_department": {
                      "id": 4,
                      "name": "Merchandising"
                    },
                    "estimated_duration_minutes": 30,
                    "priority": "important",
                    "priority_label": "Important",
                    "recurrence": null,
                    "occurrence_of": null,
                    "eligibility_window": null,
                    "on_miss": {
                      "raises_corrective_action": false
                    },
                    "on_unfulfilled": {
                      "value": "reassign_role",
                      "label": "Give it to the role holder"
                    },
                    "close_behavior": {
                      "value": "manual",
                      "label": "I close it myself"
                    },
                    "proof": {
                      "captured_in_inspection": false,
                      "photo": true,
                      "signature": false
                    },
                    "review": {
                      "mode": "none",
                      "workflow": null
                    },
                    "certifications": [],
                    "required_response": null,
                    "required_training": []
                  },
                  "shift_attention_reason": null,
                  "upcoming_shift_work": null,
                  "questions": {
                    "post_count": 2,
                    "question_count": 1,
                    "accepting_questions": true,
                    "can_post": true,
                    "max_body_length": 2000,
                    "threads": [
                      {
                        "id": 501,
                        "body": "Top shelf or eye-level?",
                        "author": {
                          "id": 31,
                          "name": "Priya Shah",
                          "photo_url": "https://ui-avatars.com/api/?name=Priya%20Shah&size=40&bold=true&color=ffffff&background=2e7b92"
                        },
                        "is_campaign_owner": false,
                        "parent_id": null,
                        "created_at": "2026-09-28T16:02:11Z",
                        "edited": false,
                        "replies": [
                          {
                            "id": 502,
                            "body": "Eye-level.",
                            "author": {
                              "id": 7,
                              "name": "Anup Rao",
                              "photo_url": "https://example.mangoapps.com/rails/active_storage/representations/redirect/eyJ.../avatar.jpg"
                            },
                            "is_campaign_owner": true,
                            "parent_id": 501,
                            "created_at": "2026-09-28T16:20:40Z",
                            "edited": false
                          }
                        ]
                      }
                    ],
                    "meta": {
                      "showing": 1,
                      "total": 1,
                      "truncated": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "App disabled/not accessible, the token lacks an execution read scope, the Campaigns surface is off (`surface_disabled`), or the caller is neither in the author tier nor a collaborator on this campaign (`forbidden`)."
          },
          "404": {
            "description": "No such campaign in this business (`not_found`)."
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:frontline_execution` scope.\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`)."
          }
        }
      }
    },
    "/esignature": {
      "get": {
        "tags": [
          "E-Signature"
        ],
        "summary": "List signature requests",
        "description": "The signature requests in the business the caller may see, newest first\n(`created_at` descending). A business admin or an E-Signature app admin\nsees every request in the business. Anyone else sees the requests they\ncreated, the requests they are a signer on (once sent — a draft is never\nshown to its signers), the requests shared with them or with a group\nthey belong to, and, for a manager, the requests their direct reports\ncreated. Policies signed in the app are left out.\n\n**Required Scopes:** `read:esignature`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:esignature\"),\nchecked after the E-Signature app gate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only requests in this state. `pending` means a request still waiting on a signer (`sent`, `viewed` or `partially_signed`, less one every signer has signed whose signed copy is still being produced, which `partially_signed` lists); `draft` means a draft that is not scheduled; `scheduled` means a draft set to send itself at a later time (the request detail's `scheduled: true`); every other value is one request status. An unrecognised value (or a repeated `status[]=`) is ignored and the list is returned unfiltered.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "scheduled",
                "draft",
                "sent",
                "viewed",
                "partially_signed",
                "completed",
                "declined",
                "voided",
                "expired"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Text to look for in the request title, or in a signer's name or email (case-insensitive substring). Empty, or a non-string value such as `search[]=`, means no search.",
            "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,
            "description": "Requests per page. Values above 50 are clamped to 50; zero, negative or non-numeric values are read as 1; an empty value uses the default.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of signature requests. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EsignatureRequestSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled` (E-Signature is not enabled for the business), `forbidden` (the app's access rules leave the caller out) or `insufficient_permissions` (\"Required scope: read:esignature\" — the token lacks the scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the E-Signature app does not exist on this platform.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/esignature/pending": {
      "get": {
        "tags": [
          "E-Signature"
        ],
        "summary": "List requests waiting for the caller's signature",
        "description": "The requests still out for signature (`sent`, `viewed` or\n`partially_signed`) on which the caller has a signer row that has not\nsigned or declined yet — matched on the caller's user id, or on their\nemail for a signer row not yet linked to a user — newest first\n(`created_at` descending). Each entry is the request summary plus the\ncaller's own place in it: their signer status, their signing order and\nwhether it is their turn now. Policies signed in the app are left out.\nThis list takes no filters.\n\n**Required Scopes:** `read:esignature`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:esignature\"),\nchecked after the E-Signature app gate.\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",
            "required": false,
            "description": "Requests per page. Values above 50 are clamped to 50; zero, negative or non-numeric values are read as 1; an empty value uses the default.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of requests waiting for the caller. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EsignaturePendingRequest"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled` (E-Signature is not enabled for the business), `forbidden` (the app's access rules leave the caller out) or `insufficient_permissions` (\"Required scope: read:esignature\" — the token lacks the scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the E-Signature app does not exist on this platform.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/esignature/templates": {
      "get": {
        "tags": [
          "E-Signature"
        ],
        "summary": "List signature templates",
        "description": "The active templates the business can send from — its own templates and\nthe platform's system templates — newest first (`created_at`\ndescending). Filter by category and search by name and description.\n\n**Required Scopes:** `read:esignature`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:esignature\"),\nchecked after the E-Signature app gate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only templates in this category (exact match). Any other string returns an empty list; empty, or a non-string value such as `category[]=`, means every category.",
            "schema": {
              "type": "string",
              "enum": [
                "hr",
                "legal",
                "sales",
                "general",
                "compliance",
                "onboarding",
                "other"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Text to look for in the template name or description (case-insensitive substring). Empty, or a non-string value, means no search.",
            "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,
            "description": "Templates per page. Values above 50 are clamped to 50; zero, negative or non-numeric values are read as 1; an empty value uses the default.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of templates. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EsignatureTemplateSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled` (E-Signature is not enabled for the business), `forbidden` (the app's access rules leave the caller out) or `insufficient_permissions` (\"Required scope: read:esignature\" — the token lacks the scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the E-Signature app does not exist on this platform.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/esignature/{id}": {
      "get": {
        "tags": [
          "E-Signature"
        ],
        "summary": "Get a signature request",
        "description": "One signature request with its signers in signing order, its fields and\nits history. The caller may read it when they are a business admin or\nan E-Signature app admin, its creator, a signer on it once it has been\nsent (a draft is never shown to its signers), the creator's manager, or\nsomeone it was shared with (directly or through a group). Anyone else\ngets the same 404 as a request that does not exist.\n\nA decline reason appears only to the people who answer for the request\nand to the signer who declined. `can_edit`, `can_send`, `can_resend`\nand `can_void` say what this caller and this token may do next.\n\n**Required Scopes:** `read:esignature`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:esignature\"),\nchecked after the E-Signature app gate.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Signature request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The signature request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "request"
                  ],
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/EsignatureRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled` (E-Signature is not enabled for the business), `forbidden` (the app's access rules leave the caller out) or `insufficient_permissions` (\"Required scope: read:esignature\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` (the E-Signature app does not exist on this platform) or `not_found` (\"Signature request not found\" — no such request in this business, or one the caller may not see).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/esignature/{id}/remind": {
      "post": {
        "tags": [
          "E-Signature"
        ],
        "summary": "Remind the signers whose turn it is",
        "description": "Sends a reminder to the signers on a request out for signature (`sent`,\n`viewed` or `partially_signed`) whose turn it is to sign — the request\npage's Remind. A signer later in the signing order is not reminded until\nit is their turn; when nobody is in turn the call answers 422. A request\nsent through an outside provider (DocuSign, Dropbox Sign) is reminded by\nthat provider, and `reminded_count` is null. The request's creator,\ntheir manager or an administrator may remind; a policy signed in the\napp is Policy Hub's record and is refused here (422 `in_app_ceremony`).\nA reminder goes out to the signers at once and cannot be unsent.\n\n**Required Scopes:** `write:esignature`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:esignature\"),\nchecked after the E-Signature app gate. The request detail's\n`can_resend` is false for such a token.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Signature request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reminders were sent. `message` says how many signers were reminded, how many later signers were left until their turn, and how many reminders could not be delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "reminded_count",
                    "failed_count",
                    "skipped_out_of_turn",
                    "request"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Reminder sent to 2 signers."
                    },
                    "reminded_count": {
                      "type": "integer",
                      "nullable": true,
                      "minimum": 0,
                      "description": "Signers reminded; null when an outside provider sends its own reminder."
                    },
                    "failed_count": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Reminders that could not be delivered."
                    },
                    "skipped_out_of_turn": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Signers later in the signing order, not reminded until it is their turn."
                    },
                    "request": {
                      "$ref": "#/components/schemas/EsignatureRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled`, `forbidden` (the app's access rules leave the caller out, or \"Only the request creator or an administrator can perform this action\") or `insufficient_permissions` (\"Required scope: write:esignature\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` (the E-Signature app does not exist on this platform) or `not_found` (\"Signature request not found\" — no such request in this business, or one the caller may not see).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`cannot_remind` (only sent, viewed or partially signed requests can be reminded), `in_app_ceremony` (a policy signed in the app — manage it in Policy Hub) or `remind_failed` (the reason, e.g. \"No one to remind — the next signer is waiting on someone earlier.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/esignature/{id}/void": {
      "post": {
        "tags": [
          "E-Signature"
        ],
        "summary": "Void a signature request",
        "description": "Voids a request that is out for signature (`sent`, `viewed` or\n`partially_signed`): its signers can no longer sign it. The request's\ncreator, their manager or an administrator may void it; a policy signed\nin the app is Policy Hub's record and is refused here (422\n`in_app_ceremony`).\n\n**Required Scopes:** `destructive:esignature`\n\nDelete-level (owner decision 2026-10-04, #6659): requires the\n`destructive:esignature` scope; `write:esignature` is not enough. A token\nwithout it gets `403 insufficient_permissions` (\"Required scope:\ndestructive:esignature\"), checked after the E-Signature app gate. The\nrequest detail's `can_void` is false for such a token.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Signature request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Why the request was voided. Trimmed and cut to 500 characters; a non-string value is ignored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was voided.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "request"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Request has been voided"
                    },
                    "request": {
                      "$ref": "#/components/schemas/EsignatureRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`app_disabled`, `forbidden` (the app's access rules leave the caller out, or \"Only the request creator or an administrator can perform this action\") or `insufficient_permissions` (\"Required scope: destructive:esignature\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` (the E-Signature app does not exist on this platform) or `not_found` (\"Signature request not found\" — no such request in this business, or one the caller may not see).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`cannot_void` (only sent, viewed or partially signed requests can be voided), `in_app_ceremony` (a policy signed in the app — manage it in Policy Hub) or `void_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/digital-signage/screens": {
      "get": {
        "tags": [
          "Digital Signage"
        ],
        "summary": "List the business's signage screens",
        "description": "Every Digital Signage screen (display) in the current business, ordered\nby name, then id (a stable order, so paging never repeats or skips a\nscreen). Filters combine (AND) and run before pagination, so\n`total_count` and `meta` count the filtered set.\n\n`status` is the runtime status a screen reports: `inactive` when an\nadmin switched it off, otherwise `never_connected` (no heartbeat yet),\n`online` (heartbeat within the last 5 minutes) or `offline`. An\nunrecognised `status` value is ignored, not refused.\n\nOnly app admins (a Digital Signage app admin, or a company admin or\nabove) may list screens, and only while the tenant's \"screen\nmanagement\" setting is on — the same rule as the web Screens tab.\n\n**Required Scopes:** `read:digital_signage` (also satisfied by `admin`\nor `read:own_digital_signage`). A token without it is refused with\n`403 insufficient_permissions` (\"Required scope: read:digital_signage\").\nA browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Runtime status. `online`, `offline` and `never_connected` match ACTIVE screens only; `inactive` matches screens an admin switched off. Any other value is ignored (no filter).",
            "schema": {
              "type": "string",
              "enum": [
                "online",
                "offline",
                "inactive",
                "never_connected"
              ]
            }
          },
          {
            "name": "online",
            "in": "query",
            "required": false,
            "description": "`true` keeps screens whose last heartbeat is within the last 5 minutes; any false-y value (`false`, `0`, an empty value) keeps the rest (never seen, or last seen longer ago). Unlike `status=online`, this ignores the screen's active flag. Omit for no filter.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "description": "Only screens bound to this location (exact match).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "active_only",
            "in": "query",
            "required": false,
            "description": "`true` keeps only active screens. Default: all screens.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Page size. Values outside 1–100 are clamped into that range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of screens. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers. A page past the last answers an empty `items`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DigitalSignageScreen"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `app_disabled` — Digital Signage is not enabled for the business.\n- `app_access_denied` — the caller has no access to the Digital Signage app.\n- `insufficient_permissions` — the token lacks the scope (\"Required scope: read:digital_signage\").\n- `screen_management_disabled` — the tenant switched screen management off.\n- `screens_access_denied` — the caller is not a Digital Signage app admin (or company admin).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Digital Signage app is not configured on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/digital-signage/media": {
      "get": {
        "tags": [
          "Digital Signage"
        ],
        "summary": "List the business's signage media assets",
        "description": "The Digital Signage media library of the current business — admin-pushed\nimage and video slides in every review state (draft, pending, approved,\nrejected) unless filtered — ordered by `position`, then oldest first.\nFilters combine (AND) and run before pagination, so `total_count` and\n`meta` count the filtered set. An unrecognised `status` or `media_kind`\nvalue is ignored, not refused.\n\nOnly media authors and reviewers may list it (a Digital Signage app\nadmin, a company admin or above, an app contributor, or a holder of the\nsignage-media approval capability), and only while the tenant's\n\"custom media\" setting is on — the same rule as the web Media tab.\n\n**Required Scopes:** `read:digital_signage` (also satisfied by `admin`\nor `read:own_digital_signage`). A token without it is refused with\n`403 insufficient_permissions` (\"Required scope: read:digital_signage\").\nA browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Review state (exact match). Any other value is ignored (no filter).",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "pending",
                "approved",
                "rejected"
              ]
            }
          },
          {
            "name": "media_kind",
            "in": "query",
            "required": false,
            "description": "Media kind (exact match). Any other value is ignored (no filter).",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video"
              ]
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "description": "Assets bound to this location PLUS assets shown at all locations (no location set).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "publishable",
            "in": "query",
            "required": false,
            "description": "`true` keeps only assets a screen may show right now: approved AND inside their `active_from` / `active_until` window. (Not applied by this filter, so an asset it keeps may still play nowhere — among others: a recurring day-of-week / time-of-day restriction; a missing file — an asset whose `media_url` is null plays nothing; the sample rule — an asset whose `is_sample_data` is true plays only on screens whose `is_sample_data` is true; and whether any screen matches its location and zone tags or keeps it inside its Max items.)",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Page size. Values outside 1–100 are clamped into that range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of media assets. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers. A page past the last answers an empty `items`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DigitalSignageMediaAsset"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `app_disabled` — Digital Signage is not enabled for the business.\n- `app_access_denied` — the caller has no access to the Digital Signage app.\n- `insufficient_permissions` — the token lacks the scope (\"Required scope: read:digital_signage\").\n- `media_disabled` — the tenant switched custom media off.\n- `media_access_denied` — the caller is neither a media author nor a media reviewer.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Digital Signage app is not configured on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/live_assist/sessions": {
      "get": {
        "tags": [
          "Live Assist"
        ],
        "summary": "List Live Assist sessions",
        "description": "The Live Assist video sessions the caller may see in their business,\nnewest first (`created_at` descending), paginated.\n\nWhich sessions are listed is the same rule the web list applies:\na caller who may view all sessions sees every session; anyone else sees\nthe sessions they host or joined as a participant, open calls that rang\nthem, live SOS calls their record admits them to (a Lone Worker alert's\nresponders), and — for a manager — every open `call_for_help` /\n`show_me` session except Lone Worker SOS calls (listed to the alert's\nresponders, above) and Remote support calls (a Service Desk call placed\nto a ticket's requester, which rings nobody else). `watch` sessions are\nlisted only to callers who may observe feeds.\n\nOptional filters: `status` and `mode`. A value outside the listed enum\nis ignored (the filter is not applied), never a 400.\n\n**Required Scopes:** `read:live_assist`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_assist\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\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": "Sessions per page. Clamped to 1..100; a blank value means 25, and a non-numeric value reads as 0 and is clamped to 1.",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only sessions in this state. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "live",
                "ended"
              ]
            }
          },
          {
            "name": "mode",
            "in": "query",
            "description": "Only sessions of this kind, as `mode_label` names them: `remote_support` lists Remote support calls, and `call_for_help` / `show_me` leave them out. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "call_for_help",
                "show_me",
                "watch",
                "huddle",
                "remote_support"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the sessions the caller may see",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LiveAssistSession"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:live_assist` (\"Required scope: read:live_assist\").\n- `app_disabled` — Live Assist is not enabled for the business.\n- `app_forbidden` — Live Assist is not published to the caller, and they neither hold a seat on a session nor are rung by, or seated as a responder on, an open one.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Live Assist app is not configured on this install.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/live_assist/sessions/{id}": {
      "get": {
        "tags": [
          "Live Assist"
        ],
        "summary": "Get a Live Assist session",
        "description": "One Live Assist session the caller may see. The same rule as the list\ndecides, plus the per-record answer the web room and details pages\ngive for a session stapled to a gated record (a Lone Worker SOS call\nadmits that alert's responders). A session the caller may not see\nanswers `404 not_found`, exactly like a missing one.\n\n**Required Scopes:** `read:live_assist`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_assist\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Session id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "session"
                  ],
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/LiveAssistSession"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:live_assist` (\"Required scope: read:live_assist\").\n- `app_disabled` — Live Assist is not enabled for the business.\n- `app_forbidden` — Live Assist is not published to the caller, and they neither hold a seat on a session nor are rung by, or seated as a responder on, an open one.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` — no session with this id in the business, or one the caller may not see (\"Session not found\").\n- `app_not_found` — the Live Assist app is not configured on this install.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:live_boards` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_boards\")\nbefore anything else is checked. The sign-in tokens hold it; a token a\nperson makes by hand does not (an authoring-tier list is every board). A\nbrowser session is asked no scope.\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 search on the board's name or the name of a visualization on it. A member matches only visualizations they are admitted to.",
            "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LiveBoard"
                          }
                        },
                        "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."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "The token lacks `read:live_boards` (`insufficient_permissions`), or Live Boards is not enabled for the business (`feature_not_enabled`) or not published to users (`not_published`)"
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:live_boards` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_boards\")\nbefore anything else is checked. The sign-in tokens hold it; a token a\nperson makes by hand does not (an authoring-tier list is every board). A\nbrowser session is asked no scope.\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": "The token lacks `read:live_boards` (`insufficient_permissions`), or Live Boards is not enabled for the business (`feature_not_enabled`) or not published to users (`not_published`)"
          }
        }
      }
    },
    "/live_boards/playlists": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "List rotations (playlists)",
        "description": "Named, ordered rotations of visualizations — what a break-room screen\nplays. Each item is one visualization (`visualization_ids`).\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\n**Required Scopes:** `read:live_boards` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_boards\")\nbefore anything else is checked. The sign-in tokens hold it; a token a\nperson makes by hand does not (an authoring-tier list is every board). A\nbrowser session is asked no scope.\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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LiveBoardPlaylist"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "The token lacks `read:live_boards` (`insufficient_permissions`), or the caller is not a Live Boards contributor or admin"
          }
        }
      }
    },
    "/live_boards/playlists/{id}": {
      "get": {
        "tags": [
          "Live Boards"
        ],
        "summary": "Get a rotation with its visualizations",
        "description": "As the list endpoint, plus `boards` — the rotation's visualizations\nresolved in play order and re-checked against the audience gate (the\nkey keeps its old name for existing clients). `visualization_ids` are\nfrozen at authoring time, so a visualization that has since been\narchived or restricted is omitted from `boards` rather than named.\n\n**Required Scopes:** `read:live_boards` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:live_boards\")\nbefore anything else is checked. The sign-in tokens hold it; a token a\nperson makes by hand does not (an authoring-tier list is every board). A\nbrowser session is asked no scope.\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 token lacks `read:live_boards` (`insufficient_permissions`), or the caller is not a Live Boards contributor or admin"
          }
        }
      }
    },
    "/lone_worker/alerts": {
      "get": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "List alerts the caller raised or responds to",
        "description": "Alerts the caller raised, alerts on rosters the caller responds for,\nand alerts the caller acknowledged or resolved (read only, kept after\nthey leave the roster), newest first. A company admin or Lone Worker app\nadmin whose token carries the WIDE `read:lone_worker` scope sees every\nalert in the tenant; an `own_`-only token keeps them to the rest. A\nworker the app monitors but does not otherwise admit is refused this\nlist (403 `app_not_accessible`); their own alerts are in\n`GET /lone_worker/safety_status` (`recent_alerts`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "`active` (raised + notifying), `open` (every alert still on its ladder — raised, notifying or acknowledged) or one alert state. `open` EXCLUDES `exhausted` alerts — the ladder ran out and no outcome is recorded yet, so nobody has confirmed the worker is safe; ask for those with `state=exhausted` and never present an empty `open` list as \"everyone is accounted for\" without it. Any other value is a 400 `invalid_state` — an unrecognised filter never silently returns every alert.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "open",
                "raised",
                "notifying",
                "acknowledged",
                "resolved",
                "exhausted"
              ]
            }
          },
          {
            "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": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of alerts. Pagination is also in the `X-Total-Count` / `X-Total-Pages` / `X-Per-Page` / `X-Current-Page` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LoneWorkerAlert"
                          }
                        },
                        "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"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "`invalid_state`."
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          }
        }
      }
    },
    "/lone_worker/alerts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "One alert, with who resolved it and its escalation trail",
        "description": "Readable by the alert's worker, a responder on its roster, a safety admin whose token carries the wide `read:lone_worker`, and the person who acknowledged or resolved it (read only — kept after they leave the roster). Anyone else gets 404, the same as for an id that does not exist. A paged responder or the worker outside the app's access groups is admitted to this one alert, as for acknowledge and resolve.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/LoneWorkerAlertDetail"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          },
          "404": {
            "description": "`not_found` — no such alert, or not one the caller may read."
          }
        }
      }
    },
    "/lone_worker/alerts/{id}/acknowledge": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Acknowledge an alert (take ownership; holds the escalation climb)",
        "description": "A responder on the alert's roster, or a company admin or Lone Worker app admin whose token carries the wide `write:lone_worker`, may acknowledge. The worker who raised the alert may not acknowledge their own. Acknowledging stops the climb and stands the other paged responders down, but only for 60 minutes: if no outcome is recorded (resolve) by then, the alert goes back to its tier and pages it again (never while Lone Worker is switched off).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/LoneWorkerAlert"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`forbidden` (not a responder, or the caller's own alert), `view_as` or `attestation_not_delegable` (acting as someone else), `insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "description": "`already_acknowledged` or `already_terminal` — someone else got there first, or the alert is closed."
          },
          "500": {
            "description": "`lone_worker_ack_failed` — our failure; retry."
          }
        }
      }
    },
    "/lone_worker/alerts/{id}/release": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Hand an acknowledgement back (undo a mistaken acknowledge)",
        "description": "The responder who acknowledged the alert, or a company admin or Lone Worker app admin, puts it back on its responder ladder at the same tier. That tier's other responders are paged again (the releaser is not); when nobody else is on it, the alert climbs to the next tier at once, and when no later tier is left it is exhausted and the safety admins are alerted. The worker is told, and the audit trail records who released it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Released — the alert's `state` is `notifying` again, or `exhausted` when nobody on this or a later tier was left to page (the safety admins are alerted).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/LoneWorkerAlert"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`forbidden` (not the acknowledger, nor a company admin or Lone Worker app admin whose token holds the wide `write:lone_worker` scope), `release_not_permitted` (the acknowledgement changed hands meanwhile), `view_as` or `attestation_not_delegable` (acting as someone else), `insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "description": "`not_acknowledged` — the alert is not acknowledged (already handed back, resolved or exhausted); `sample_alert` — a sample alert never pages anyone, so there is nothing to hand back."
          },
          "500": {
            "description": "`lone_worker_release_failed` — our failure; retry."
          }
        }
      }
    },
    "/lone_worker/alerts/{id}/resolve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Resolve an alert",
        "description": "A responder on the alert's roster, or a company admin or Lone Worker app admin whose token carries the wide `write:lone_worker`, may resolve with any outcome. The worker who raised the alert may only cancel it as `false_alarm`. An `incident` resolution requires a note, and opens a Safety Hub incident unless the tenant has turned that hand-off off (`incident_id` is then null).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "resolution"
                ],
                "properties": {
                  "resolution": {
                    "type": "string",
                    "enum": [
                      "safe",
                      "false_alarm",
                      "incident"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "Required when `resolution` is `incident`. Stored up to 5,000 characters."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "alert": {
                      "$ref": "#/components/schemas/LoneWorkerAlert"
                    },
                    "incident_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The Safety Hub incident an `incident` resolution created."
                    },
                    "incident_handoff_failed": {
                      "type": "boolean",
                      "description": "True when an `incident` resolution tried to open a Safety Hub incident and could not. The alert is still resolved; `incident_id` is null and the incident must be reported in Safety Hub by hand."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`forbidden`, `view_as` or `attestation_not_delegable` (closing the alert or recording its outcome as someone else), `insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "description": "`already_resolved`."
          },
          "422": {
            "description": "`invalid_resolution`, `note_required` or `invalid_note` (a note that is not a string)."
          },
          "500": {
            "description": "`lone_worker_resolve_failed` — our failure; retry."
          }
        }
      }
    },
    "/lone_worker/safety_status": {
      "get": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "The caller's own safety status",
        "description": "Whether the caller is clocked in, their armed check-in (always returned, so the \"I'm safe\" action stays reachable; `check_in_due` says whether it is awaiting an answer now, `responders_alerted_at` when responders are paged if it is not answered, `past_due` whether that moment has passed), their in-progress alert, if any — an acknowledged alert is still in progress — and, when none is in progress, their own alert from the last 24 hours whose ladder ran out with no outcome recorded (`exhausted_alert`): never present the card as all clear while it is set. `recent_alerts` are the caller's own five most recent alerts, unresolved first (`recent_alerts_total` is how many they have in all), so a client can reach any of them to close it as a false alarm.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Safety status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "on_shift": {
                      "type": "boolean"
                    },
                    "check_ins": {
                      "type": "string",
                      "enum": [
                        "armed",
                        "paused",
                        "ended",
                        "ending",
                        "not_covered",
                        "off_shift"
                      ],
                      "description": "Where the worker stands for timed check-ins: armed (a check-in is armed), paused (the worker's SOS or missed check-in alert is open; check-ins restart once it closes), ended (clocked in past a covered shift's scheduled end, so check-ins have stopped), ending (a covered shift ends within one grace window, so no further check-in is armed), not_covered (clocked in, no responder roster covers the shift), off_shift (not clocked in)."
                    },
                    "check_ins_ended_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "The covered shift's scheduled end, when check_ins is ended or ending."
                    },
                    "armed_check_in": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LoneWorkerCheckIn"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "responders_alerted_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When responders are paged if this check-in is still unanswered (due time plus grace) — the deadline the check-in push states."
                            },
                            "past_due": {
                              "type": "boolean",
                              "description": "True once `responders_alerted_at` has passed. An answer now is recorded as a missed check-in and raises the alert; the worker then closes it as a false alarm."
                            }
                          }
                        }
                      ],
                      "nullable": true
                    },
                    "check_in_due": {
                      "type": "boolean",
                      "description": "True while the armed check-in awaits an answer. Always false while `check_ins` is `paused` (the worker's own alert is open: nothing is prompted, and a lapse is recorded on that alert)."
                    },
                    "active_alert": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LoneWorkerAlert"
                        }
                      ],
                      "nullable": true
                    },
                    "exhausted_alert": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LoneWorkerAlert"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "exhaustion_kind": {
                              "type": "string",
                              "enum": [
                                "abandoned",
                                "handed_back",
                                "lapsed",
                                "unstaffed",
                                "unanswered"
                              ],
                              "description": "Why no outcome is recorded: `unanswered` — nobody acknowledged in time; `lapsed` — a responder acknowledged but did not close it, and nobody acknowledged after it was escalated again; `handed_back` — the responder who acknowledged it stopped handling it (handed it back, or was taken off the roster or left), and nobody on that tier or above was left to page; `unstaffed` — there was nobody to page; `abandoned` — acknowledged but never closed, so it was closed automatically."
                            }
                          }
                        }
                      ],
                      "nullable": true,
                      "description": "Null while an alert is in progress, and for alerts raised more than 24 hours ago or seeded as sample data. The worker can close it as a false alarm (POST /lone_worker/alerts/{id}/resolve)."
                    },
                    "recent_alerts": {
                      "type": "array",
                      "maxItems": 5,
                      "items": {
                        "$ref": "#/components/schemas/LoneWorkerAlert"
                      }
                    },
                    "recent_alerts_total": {
                      "type": "integer",
                      "description": "How many alerts are filed under the caller in all: the ones they raised, and any an unassigned safety device filed under them."
                    },
                    "capture_location_on_sos": {
                      "type": "boolean",
                      "description": "Whether #sos stores the location the client sends (the tenant's setting)."
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          }
        }
      }
    },
    "/lone_worker/sos": {
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Raise an SOS",
        "description": "One live SOS per worker: when the caller already has one in progress it is returned with `already_active: true` and 200, and no second ladder starts. Location is stored only when the tenant's \"Capture Location When SOS Is Triggered\" setting is on; unparseable or out-of-range coordinates are dropped, never rejected.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lat": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 90
                  },
                  "lng": {
                    "type": "number",
                    "minimum": -180,
                    "maximum": 180
                  },
                  "accuracy": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Location accuracy in metres, stored as a whole number; a value above 100000 is dropped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's SOS already in progress (`already_active` is true)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoneWorkerRaiseResult"
                }
              }
            }
          },
          "201": {
            "description": "Raised, and responders are being paged (`reached_responders` true)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoneWorkerRaiseResult"
                }
              }
            }
          },
          "202": {
            "description": "Raised, but NO responder could be reached (`reached_responders` false; `message` tells the worker to call emergency services). Never present this as \"help is on the way\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoneWorkerRaiseResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`view_as` or `attestation_not_delegable` (raising an SOS as someone else), `insufficient_permissions`, `app_not_enabled` or `app_not_accessible`. While Lone Worker is switched off, `app_not_enabled` says the SOS was NOT sent and to call the local emergency number — or that the caller's own SOS is still open (`details.open_alert_id`) — and nothing is raised."
          },
          "500": {
            "description": "`lone_worker_raise_failed` — our failure; retry (a retry never starts a second SOS)."
          }
        }
      }
    },
    "/lone_worker/floor_issue": {
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Report a floor issue (line jam)",
        "description": "Rides the same roster escalation as an SOS, without location. The note is required and is read to responders, truncated to 280 characters.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note"
                ],
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Stored and read to responders truncated to 280 characters; a longer note is accepted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Raised, and responders are being paged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoneWorkerRaiseResult"
                }
              }
            }
          },
          "202": {
            "description": "Raised, but no responder could be reached (`reached_responders` false)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoneWorkerRaiseResult"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`view_as` or `attestation_not_delegable` (reporting as someone else), `insufficient_permissions`, `app_not_enabled` or `app_not_accessible`. While Lone Worker is switched off, `app_not_enabled` says the floor issue was NOT reported and to call the local emergency number if anyone is in danger, and nothing is raised."
          },
          "422": {
            "description": "`note_required`, or `invalid_note` (a note that is not a string)."
          },
          "500": {
            "description": "`lone_worker_raise_failed` — our failure; retry."
          }
        }
      }
    },
    "/lone_worker/check_in/answer": {
      "post": {
        "tags": [
          "Lone Worker"
        ],
        "summary": "Answer a check-in (\"I'm safe\")",
        "description": "Answers the given check-in, or the caller's earliest armed one when `check_in_id` is omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "check_in_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered. `check_in` is the check-in just answered (state `ok`), not the next one; read the next armed check-in from `GET /lone_worker/safety_status`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "check_in": {
                      "$ref": "#/components/schemas/LoneWorkerCheckIn"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "`insufficient_permissions`, `app_not_enabled` or `app_not_accessible`."
          },
          "409": {
            "description": "`check_in_missed` — the window already closed and a missed-check-in alert was raised (`details.alert_id`); the message tells the worker to close it as a false alarm if they are safe. With `details.folded: true` nothing new was raised — the miss was added to the worker's own open SOS or missed-check-in alert (`details.alert_id`), whose responders are already alerted; show the message and do NOT offer to close that alert as a false alarm. `check_in_missed_raise_failed` — the window closed long enough ago that the alert could not be raised and will not be retried; show the message (tell a supervisor now) and do not retry."
          },
          "422": {
            "description": "`no_armed_check_in` — nothing is awaiting an answer; also when the check-in was already missed and its alert has since been closed (the message says nothing else was raised)."
          },
          "500": {
            "description": "`lone_worker_check_in_answer_failed`, or `check_in_missed_raise_pending` — the window had closed and the missed-check-in alert could not be raised yet (the check-in is re-armed and retried automatically). Our failure; retry."
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\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. `results` lists the live or closed surveys whose full results the caller may read (the web Results index's rule — survey administrators, a results or co-owner grant, or the people the organization names to view every survey's results); a caller with none of these gets a `403`. Any other value — including a different case, or a list-shaped value — is a 400 `invalid_scope`.",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "results"
              ]
            }
          },
          {
            "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SurveySummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/SurveysPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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* `insufficient_permissions` — the API token lacks `read:surveys`\n  (\"Required scope: read:surveys\", #7183).\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/shared_summaries": {
      "get": {
        "tags": [
          "Surveys"
        ],
        "summary": "List the \"You said, we did\" summaries shared with me",
        "description": "The \"You said, we did\" summaries the people who ran a survey shared with\nthe caller, newest first — what the Surveys home (web and phone) shows\nunder \"You said, we did\", and what Ask AI's\n`get_shared_survey_summaries` returns, in the same keys.\n\n**Who sees what** is the summary's own reader\n(`Surveys::PublishedSummary.recent_for`): a summary is listed only when\nit was SENT to the caller — their own receipt of it, written when it\nwent out — never because they are in the survey's audience today. So\nsomeone who joined after it was shared does not see it. A survey made\nconfidential since is listed to nobody. A full-results reader of a\nsurvey whose summary was not sent to them reads it on\n`GET /surveys/surveys/{id}/results` (`shared_summary`) instead.\n\n**The window and the cap** are the Surveys home's: summaries sent in\nthe last 90 days, at most 5 — a summary is news. The list is not\npaginated; an empty `items` means none was sent to the caller in that\nwindow.\n\nEach summary is the organization's own words, never anything an\nanswer said, so no anonymity floor applies.\n\nUse this to show me what changed after a survey, read the results\nsummaries shared with me, or see \"you said, we did\".\n\n**Required Scopes:** `read:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The summaries shared with the caller, newest first (possibly none)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items",
                        "total_count"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SurveySharedSummary"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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* `insufficient_permissions` — the API token lacks `read:surveys`\n  (\"Required scope: read:surveys\", #7183).\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). At\n  `:aggregate`, on an anonymous survey below its response floor,\n  `response_count`, `completion_rate`, `responded_from_audience` and\n  `responses_outside_audience` are `null`: a run's audience is a handful\n  of named people, so the count says whether they answered (the web\n  masks the same numbers).\n* `:team` — `response_count` ONLY, counting the caller's team (direct\n  reports, or everyone below them with `reach=all`), and `null` when an anonymous survey's team count sits below\n  the tenant's anonymity floor — or, on an anonymous survey that shares\n  its results with employees, when fewer people than the floor answered\n  outside the team (the whole-survey count minus the team's would say\n  how many of them did). 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\n**Required Scopes:** `read:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). An invited respondent's read asks it too. A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "reach",
            "in": "query",
            "required": false,
            "description": "`:team` tier only — whose answers the team slice covers: `direct` (the caller's direct reports, the default — everyone below the caller when no direct report is left) or `all` (everyone below the caller in this business's reporting line). On a survey where fewer people below the direct reports answered than the anonymity floor, `all` falls back to the direct reports, so the two slices can never be subtracted to isolate a few people. Ignored at other tiers.",
            "schema": {
              "type": "string",
              "enum": [
                "direct",
                "all"
              ],
              "default": "direct"
            }
          }
        ],
        "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), `forbidden` (the caller does not have access to the Surveys app), or `insufficient_permissions` (the API token lacks `read:surveys`, #7183).",
            "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\n**Required Scopes:** `write:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). An invited respondent's answer asks it too. A token without it gets `403 insufficient_permissions`\n(\"Required scope: write:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\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_permissions` — the API token lacks `write:surveys`\n  (\"Required scope: write:surveys\", #7183).\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* `conversation_survey` — the survey is answered in a conversation\n  with the AI interviewer (`response_mode: conversation`), never as\n  form answers. `error.details.take_url` is the take page, which\n  starts or resumes the conversation. Nothing was saved.\n\nA required question left blank is a `validation_error` in `errors`:\nits `field` is `base`, its `message` names the question by its label,\nand its `field_name` is the question's key, so a client can mark the\nquestion it left blank. Nothing was saved.\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"
                              },
                              "code": {
                                "type": "string",
                                "description": "Always `validation_error`"
                              },
                              "field_name": {
                                "type": "string",
                                "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/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 team —\ndirect reports, or everyone below them with `reach=all` — and reports\n`scope: \"team\"`; `:none` is a `403`.\nConfidential surveys and the per-survey manager opt-in are honoured by\nthe policy itself.\n\n**A manager on a survey that shares its results with employees** reads\nthe aggregate by default — exactly what the people they manage read —\nand their team's slice with `scope=team`, the web and phone pages'\n\"All responses\" / \"Your team\". `team_view_available: true` says the\ncaller may ask for it. That team slice is also compared with the whole\nsurvey: a numeric or choice question whose answers differ from the whole\nsurvey's by fewer people than the floor comes back\n`insufficient_data: true`, so subtracting the two never isolates a few\npeople outside the team. And on an anonymous survey, a team slice that\nfewer people than the floor answered OUTSIDE of (none included) comes\nback like one below the floor — `response_count: null`,\n`anonymity_protected: true`, `question_summaries: []` — but with\n`threshold_met: true`: its participation would otherwise say how many of\nthose few people answered.\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 any survey whose response count (for this scope) is below the\n   threshold — anonymous or not, as the web and phone results pages\n   hold them back — 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   on an ANONYMOUS survey a free-text question with fewer answers than\n   the threshold is marked `insufficient_data: true` as well.\n\nNon-anonymous surveys are not suppressed at the survey level (1 and 2 do\nnot apply), but per-question suppression (3) still does. These are the\nsame rules the web and phone results pages apply\n(`Surveys::ResultsSummary`).\n\nThis endpoint is not paginated: the whole result set for the caller's\nscope is rolled up in one pass.\n\n**Required Scopes:** `read:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "reach",
            "in": "query",
            "required": false,
            "description": "`:team` tier only — whose answers the team slice covers: `direct` (the caller's direct reports, the default — everyone below the caller when no direct report is left) or `all` (everyone below the caller in this business's reporting line). On a survey where fewer people below the direct reports answered than the anonymity floor, `all` falls back to the direct reports, so the two slices can never be subtracted to isolate a few people. Ignored at other tiers.",
            "schema": {
              "type": "string",
              "enum": [
                "direct",
                "all"
              ],
              "default": "direct"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Omit for the results the caller's tier reads by default. `team` asks for the caller's team slice: a manager's only view on a survey that does not share its results with employees, and an extra view beside the whole-survey aggregate on one that does (`team_view_available`). A caller with no team slice of this survey gets a `403`; any other value — including a list-shaped one — is a 400 `invalid_scope`.",
            "schema": {
              "type": "string",
              "enum": [
                "team"
              ]
            }
          }
        ],
        "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"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_scope` — `scope` is neither absent nor `team`, or is list-shaped. `error.details.accepted_values` names the set.",
            "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": "* `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* `forbidden` — `scope=team` was asked for and the caller has no\n  team slice of this survey.\n* `insufficient_permissions` — the API token lacks `read:surveys`\n  (\"Required scope: read:surveys\", #7183).\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**Four 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. A survey answered by conversation with the AI interviewer is\nrefused while new conversations can't start — conversation mode is off\nfor the organization, or its AI usage is blocked — exactly as the web's\nSend Reminders refuses it. 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 something to send**, computed\nexactly as the web twin and the Surveys agent compute them, so the doors\ncannot disagree about who counts as a recipient: an empty target\naudience, an audience where everyone has already responded, and a\nreminder none of whose non-responders can open the Surveys app.\n\nA reminder reaches each non-responder who can open the Surveys app in\ntheir Inbox (an actionable item), and by email when they have an address\non file — people without an email address are reached. Each reminder\nfiles a fresh \"Reminder:\" Inbox item and retires the invitation item the\nperson held (\"A newer reminder about this survey replaced this one.\"), so\na person holds one open item for the survey.\n\n**Required Scopes:** `write:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: write:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\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",
                    "emailed_count",
                    "already_in_inbox_count",
                    "message"
                  ],
                  "properties": {
                    "accepted": {
                      "type": "boolean",
                      "example": true
                    },
                    "survey_id": {
                      "type": "integer"
                    },
                    "recipient_count": {
                      "type": "integer",
                      "description": "Non-responders the reminder reaches — each gets a fresh Inbox item, and an email when they have an address.",
                      "example": 12
                    },
                    "emailed_count": {
                      "type": "integer",
                      "description": "The part of `recipient_count` with an email address on file — the number that will be mailed.",
                      "example": 3
                    },
                    "already_in_inbox_count": {
                      "type": "integer",
                      "description": "Deprecated — always 0. It counted non-responders a reminder gave nothing new; a reminder now files a fresh Inbox item for everyone, so there are none. Kept so existing clients parse.",
                      "deprecated": true,
                      "example": 0
                    },
                    "message": {
                      "type": "string",
                      "example": "Reminder delivery has been queued for 12 employees who haven't responded yet — 3 by email and 9 in Notifications."
                    }
                  }
                }
              }
            }
          },
          "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_permissions` — the API token lacks `write:surveys`\n  (\"Required scope: write:surveys\", #7183).\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* `conversation_unavailable` — the survey is answered by\n  conversation with the AI interviewer, and new conversations\n  can't start (conversation mode off, or the organization's AI\n  usage blocked); `error.message` says which, in the web's words.\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 can open\n  the Surveys app.\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\n**Required Scopes:** `write:surveys` (#7183, owner rule 2026-10-06 \"every\napi/v1 door asks a scope\"; the pair Ask AI and MCP ask for the Surveys\nagent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: write:surveys\") before anything else is checked. The sign-in\ntokens hold it; a browser session is asked no scope.\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_permissions` — the API token lacks `write:surveys`\n  (\"Required scope: write:surveys\", #7183).\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"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews": {
      "get": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "The caller's conversations",
        "description": "The caller's open conversations, and the ones they sent in the last 30\ndays, newest activity first. Named conversations only; never a\nrehearsal (\"Try it\" belongs to the builder that started it), and never\none whose app no longer runs conversations.\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": "Rows per page, 1–100 (default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ai_interviews": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AiInterviewSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks read:ai_interviews.",
            "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": [
          "AI Interviews"
        ],
        "summary": "Start a conversation, or resume the one open",
        "description": "Starts the caller's conversation on an app's record — or resumes the\none they already have open on it — through the app's own door, the\nsame one its web page asks. `context_type` is a record type an app\nruns conversations on (an unknown one answers the list). 201 when it\nstarted — a new conversation opens on `screen: \"opening\"` until POST\n…/begin; 200 when an open one was resumed — it opens where it stands\n(its `screen`: the conversation, the review…).\n\nRefusals: `conversation_unavailable` (the app's door is shut — the\nsurvey closed, the exit interview was completed, the shift has\nfeedback), `already_participated`, `ai_blocked` (the organization's AI\nis blocked, so a NEW conversation can't start; an open one is still\nresumed), `anonymous_on_web` (the conversation would not record who\nthe person is, so it is held on the web: `details.take_url` is the\napp's own page for it, when the app names one), `start_failed`,\n`unknown_context_type`. A record that is not in this tenant answers\n`conversation_unavailable`, the same as one the caller may not take, so\nthe API never confirms which records exist.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "context_type",
                  "context_id"
                ],
                "properties": {
                  "context_type": {
                    "type": "string",
                    "description": "The record type, e.g. `Surveys::Survey`, `RecruitingOffboardingPlan`, `AttendanceRecord`.",
                    "example": "Surveys::Survey"
                  },
                  "context_id": {
                    "type": "integer",
                    "example": 812
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation already open, resumed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "201": {
            "description": "A new conversation, on its opening screen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks write:ai_interviews; `private_conversation` — an impersonation token, or a session viewing as the person (see AiInterviewRefusal).",
            "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_participated` — the person already took part.",
            "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": "`conversation_unavailable`, `ai_blocked`, `anonymous_on_web` (with `details.take_url`), `start_failed` or `unknown_context_type` (with `details.context_types`).",
            "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": {
                  "anonymous_on_web": {
                    "value": {
                      "error": {
                        "code": "anonymous_on_web",
                        "message": "This conversation doesn't record who you are, so it is held on the web, not in this app. Open it in a browser.",
                        "details": {
                          "take_url": "https://acme.workforce.mangoapps.com/apps/surveys/take/812"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "One conversation, as its page shows it",
        "description": "The screen (`opening`, `conversation`, `coding` while the review is\ndrafted — poll this —, `review`, `done`, or `closed` when the app's own\ndoor shut on an unsent conversation, which may still be deleted), the\ntranscript, topic progress, what the composer offers now and, on the\nreview, what will be recorded. A read changes nothing, so it may be\npolled: a returning person's sitting opens with their next turn.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks read:ai_interviews; `private_conversation` — an impersonation token, or a session viewing as the person (see AiInterviewRefusal).",
            "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "End — delete the conversation and everything in it",
        "description": "Before it is sent only; a sent conversation is the app's record (`already_sent`). A legal hold keeps it (`not_deleted`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    },
                    "id": {
                      "type": "integer"
                    },
                    "return_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "The app's own page the conversation was for."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/begin": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "Start (the consent), or start the next topic",
        "description": "\"Start\" on the opening screen: the person agrees to what the opening\nsaid (`opening`, `about`), and the interviewer asks the first question.\nBetween the sittings of a conversation held one topic per sitting\n(`composer.between_sittings`), it starts the next topic.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/turn": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "A typed answer",
        "description": "The person's words are saved before the interviewer is asked. When the\ninterviewer can't answer (`reply_failed`, 502) the words are kept and\n`composer.awaiting_reply` is true: answer with POST …/retry, never by\nsending the same turn again.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "said"
                ],
                "properties": {
                  "said": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What the person wrote."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "502": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/choose": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "A tapped answer to the question being asked",
        "description": "Recorded with no model call. Only while `composer.item` is that measure (`moved_on` otherwise).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "item_key",
                  "said"
                ],
                "properties": {
                  "item_key": {
                    "type": "string",
                    "description": "`composer.item.key`."
                  },
                  "said": {
                    "description": "One of `composer.item.options[].value` — a list of them when `multiple`.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "integer"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/skip": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "Skip the question being asked",
        "description": "Never re-asked, no reason needed. A question the app needs answered has `can_skip` false and is refused (`needs_answer`). Send `item_key` (`composer.item.key`) to skip only the question you saw — once the conversation has moved on it is refused (`moved_on`) rather than skipping the next one.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "item_key": {
                    "type": "string",
                    "description": "`composer.item.key`, the question being skipped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/retry": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "Try again after the interviewer didn't answer",
        "description": "Answers the saved words (`composer.awaiting_reply`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "502": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/speak": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "Hold to talk — one recorded clip as an answer",
        "description": "Offered while `composer.transports` includes `recording`. The clip is\ntranscribed and answered as a turn; it is never attached or kept. Up to\n20 MB, sent as a FILE in the multipart `audio` field — not as text\n(base64 is refused). Refusals: `audio_missing` (no file in `audio`;\nnothing was transcribed), `no_speech` (the clip held no words),\n`clip_too_large` (413), `voice_unavailable` (503),\n`recording_not_offered`. Live voice is not offered through the API.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "audio"
                ],
                "properties": {
                  "audio": {
                    "type": "string",
                    "format": "binary",
                    "description": "The clip (audio/webm, audio/mp4, audio/mpeg, audio/wav …)."
                  },
                  "duration_ms": {
                    "type": "integer",
                    "description": "How long the person spoke",
                    "as the recorder measured it.": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the spoken answer",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/AiInterviewResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "transcript": {
                          "type": "string",
                          "description": "What was heard."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "413": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "502": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "503": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/review": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "One change at \"Here's what we'll record\"",
        "description": "While `screen` is `review`. One of: a verdict on an answer\n(`answer_id` + `verdict`: `confirm` — Looks right, `edit` — their own\nwords in `said`, a string, replace the summary, `remove`, or `value` —\na measure's answer changed to `said`); a required question the\nconversation left unanswered (`missing_item` + `said`: a string for an\nopen question, one of its options — a list of them when `multiple` —\nfor a measure); the quote choice (`quote_consent`, one of\n`review.quote_choices`; ignored when that list is empty — the app\nnever quotes anyone); \"Did this capture what you think?\"\n(`felt_captured`, a whole number 1–7, or null to clear). A request\nnaming none of these is refused (`nothing_to_change`), as is `said`\nthat is not a string where words are asked for (`write_something`,\n`write_an_answer`) and a `felt_captured` off the scale\n(`felt_captured`). Each part is applied on its own: a `quote_consent`\nor `felt_captured` sent beside a refused verdict is still kept, and\nthe first refusal is the answer.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "answer_id": {
                    "type": "integer"
                  },
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "confirm",
                      "edit",
                      "remove",
                      "value"
                    ]
                  },
                  "missing_item": {
                    "type": "string"
                  },
                  "said": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "integer"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  },
                  "quote_consent": {
                    "type": "string",
                    "enum": [
                      "none",
                      "deidentified",
                      "with_name"
                    ],
                    "description": "One of `review.quote_choices`; ignored when that list is empty."
                  },
                  "felt_captured": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 7,
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/submit": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "Send",
        "description": "Sends what the review shows; untouched answers count as Looks right.\n`quote_consent` and `felt_captured` may ride along, as PATCH …/review\ntakes them. Refusals: `missing_required` (422, with\n`details.missing`: the item keys still needing an answer),\n`already_participated`, `closed` (409), `invalid` (422 — what was sent\ncould not be saved), `not_sent` (422 — refused with no code of its\nown), or the app's own refusal under its code (`already_given`, 409,\nwhen the shift already has feedback).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quote_consent": {
                    "type": "string",
                    "enum": [
                      "none",
                      "deidentified",
                      "with_name"
                    ],
                    "description": "One of `review.quote_choices`; ignored when that list is empty."
                  },
                  "felt_captured": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 7,
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/ai_interviews/{id}/language": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The conversation's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "AI Interviews"
        ],
        "summary": "The conversation's language — the interviewer follows",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "language"
                ],
                "properties": {
                  "language": {
                    "type": "string",
                    "example": "es"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The conversation after the change — what GET /ai_interviews/{id} answers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused — the codes are AiInterviewRefusal's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiInterviewRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/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's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "customer-onboarding"
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "One template",
        "description": "One workspace template, with its full `structure` blueprint.\nAddressable by numeric id OR by the `slug` the list hands out — the\nsame reference `template` on `POST /workspace/workspaces` accepts.\n\nA slug is looked up among the system templates first, then the\ntenant's own, and only then is an all-digit value read as an id. A\ntenant template may reuse a system template's slug; the system one\nwins that lookup, so use the numeric `id` to reach the tenant's copy.\n\nThe response adds `template.plan`: every to-do and milestone of a\ndated template with its day offsets from the anchor date and, when\n`anchor_date` is given, its dates — exactly what `POST\n/workspace/workspaces` with the same `anchor_date` writes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "anchor_date",
            "in": "query",
            "required": false,
            "description": "The anchor date (YYYY-MM-DD) to date the plan preview from — go-live, opening day, the deadline. An unreadable date is `422`.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2027-03-01"
          }
        ],
        "responses": {
          "200": {
            "description": "The template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "template"
                  ],
                  "properties": {
                    "template": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceTemplate"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "plan": {
                              "$ref": "#/components/schemas/WorkspaceTemplatePlan"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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 or slug is visible to this business (error code `not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/portfolio": {
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "The portfolio — every project the caller may see, with its health",
        "description": "The portfolio (employee-experience PM-4): the projects the caller may\nsee, with the owner's health beside what the data shows, filtered and\npaged exactly as the web and mobile Portfolio pages are\n(Workspaces::PortfolioQuery).\n\nWHO SEES WHAT (`reach` in the response): a Workspace app admin sees\nevery workspace (`tenant`); someone who may open Workspace Reports sees\nevery workspace that is not private plus their own (`reportable`);\neveryone else sees only the workspaces they can open (`own`). A row the\ncaller may see but not open has `can_open: false`.\n\n`health_counts` and `signal_counts` cover EVERY matching project, not\njust this page: `health_counts` over every filter but `health`,\n`signal_counts` over every filter but `signal` — each is exactly the\ntotal the same request with that filter would return. A signal whose\nsection (to-dos, check-ins) is switched off business-wide has no key.\n\nAdvanced planning: below Platform Standard the endpoint answers `402`\nwith error code `upgrade_required`.\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`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Words in the name or description, the owner's name (or email, where the organization's directory shows email to the caller), or `#123` for workspace 123.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "name": "health",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "on_track",
                "at_risk",
                "off_track",
                "not_set"
              ]
            }
          },
          {
            "name": "signal",
            "in": "query",
            "required": false,
            "description": "Only projects where the data shows this.",
            "schema": {
              "type": "string",
              "enum": [
                "missed_milestones",
                "high_risks",
                "overdue_tasks",
                "check_in_due",
                "open_risks"
              ]
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "description": "The location and every location beneath it.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "updated",
                "name",
                "ends_on",
                "health"
              ],
              "default": "updated"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's portfolio",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspaces",
                    "total_count",
                    "meta",
                    "reach",
                    "filters",
                    "health_counts",
                    "signal_counts"
                  ],
                  "properties": {
                    "workspaces": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "archived"
                            ]
                          },
                          "kind": {
                            "type": "string",
                            "nullable": true
                          },
                          "health": {
                            "type": "string",
                            "enum": [
                              "on_track",
                              "at_risk",
                              "off_track",
                              "not_set"
                            ]
                          },
                          "health_signals": {
                            "type": "object",
                            "description": "overdue_tasks, check_in_due, missed_milestones, open_risks, high_risks and a tone."
                          },
                          "department": {
                            "type": "string",
                            "nullable": true
                          },
                          "owner": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "sponsor": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "starts_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "ends_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "can_open": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    },
                    "reach": {
                      "type": "string",
                      "enum": [
                        "tenant",
                        "reportable",
                        "own"
                      ]
                    },
                    "filters": {
                      "type": "object",
                      "description": "The filters applied",
                      "normalised (defaults left out).": null
                    },
                    "health_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "signal_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The organization's plan does not include advanced planning (error code `upgrade_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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/workload": {
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "Workload — people × weeks, each person's to-dos against their hours",
        "description": "Workload (employee-experience PM-7): for each person the caller may see,\ntheir open Workspace to-dos week by week against the hours they have,\npaged and ordered exactly as the web and mobile Workload pages are\n(Workspaces::WorkloadQuery).\n\nHOURS: a to-do's estimate spread evenly over the weekdays of its span\n(start to due day), from today on. CAPACITY: the person's contract for\nthat week of its pay-period cycle, a manager's target on a schedule\ncovering the week, the role's expected hours, or the standard 40 —\nless approved leave, and in the current week only the weekdays left.\n`capacity_source` says which. A to-do with no estimate is counted in\n`todo_count` and `unestimated_count`, never timed.\n\nWHO SEES WHOM (`reach`): the portfolio's rule — a Workspace app admin\neveryone working in a project (`tenant`), a report viewer the people in\nevery project that is not private plus their own (`reportable`),\neveryone else the people in their own projects (`own`) — plus, always,\nthe people the caller manages. A person's load counts every open\nto-do they hold; this endpoint never returns a to-do's title.\n\nTo move work, `PATCH /workspace/workspaces/{id}/tasks/{task_id}` with\n`assignee_id`.\n\nAdvanced planning: below Platform Standard the endpoint answers `402`\nwith error code `upgrade_required`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "People per page. Maximum 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Alias for `per_page`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Words in the person's name (or email, where the organization's directory shows email to the caller).",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "name": "weeks",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                4,
                6,
                8,
                12
              ],
              "default": 6
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "A date in the first week shown (never before this week). Default this week.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "`load` — most estimated hours in the window first; `name` — alphabetical.",
            "schema": {
              "type": "string",
              "enum": [
                "load",
                "name"
              ],
              "default": "load"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's workload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "people",
                    "weeks",
                    "total_count",
                    "meta",
                    "reach",
                    "filters"
                  ],
                  "properties": {
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "open_todos": {
                            "type": "integer",
                            "description": "Every open task the person holds — every project and the Tasks app."
                          },
                          "past_due_count": {
                            "type": "integer",
                            "description": "Open Workspace to-dos whose last day has passed."
                          },
                          "undated_count": {
                            "type": "integer",
                            "description": "Open Workspace to-dos with no date."
                          },
                          "weeks": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "starts_on": {
                                  "type": "string",
                                  "format": "date"
                                },
                                "load_hours": {
                                  "type": "number"
                                },
                                "capacity_hours": {
                                  "type": "number"
                                },
                                "capacity_source": {
                                  "type": "string",
                                  "enum": [
                                    "contract",
                                    "schedule",
                                    "role_default",
                                    "default"
                                  ]
                                },
                                "utilisation": {
                                  "type": "number",
                                  "nullable": true,
                                  "description": "load ÷ capacity; null when the week has no hours."
                                },
                                "band": {
                                  "type": "string",
                                  "enum": [
                                    "free",
                                    "light",
                                    "balanced",
                                    "stretched",
                                    "over"
                                  ]
                                },
                                "todo_count": {
                                  "type": "integer"
                                },
                                "unestimated_count": {
                                  "type": "integer"
                                },
                                "leave_days": {
                                  "type": "number",
                                  "description": "Weekdays of approved leave (a half day is 0.5)."
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "weeks": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "date"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    },
                    "reach": {
                      "type": "string",
                      "enum": [
                        "tenant",
                        "reportable",
                        "own"
                      ]
                    },
                    "filters": {
                      "type": "object",
                      "description": "The filters applied",
                      "normalised (defaults left out).": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The organization's plan does not include advanced planning (error code `upgrade_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The Workspace app is not enabled for the business or not accessible to the caller, or Workspace to-dos are switched off for the business (\"Tasks are disabled for this business.\") — error code `forbidden`.",
            "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`, `draft` (the drafts you own — not launched yet), or `all`. An unrecognised value behaves as `active`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "draft",
                "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\n**From a template.** Send `template` (a slug or id from\n`GET /workspace/templates`) to build the workspace the way the web\ncreate drawer does: the template's sections, lists and seed content\nare applied, and the template's own `kind` is the default unless you\nsend one. Content the template declares but that could not be applied\ndoes not fail the call — the workspace is created and `201` carries a\n`warnings` list naming what is missing. An unknown template is `422`.\n\n`kind`, `tags`, `ends_on` and `series_cadence` are checked by the same\nrules the web, the Ask AI agent and Smart Import apply; a value outside\nthem is `422` naming the accepted values.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours. A retry carrying the same key replays the original response (header `X-Idempotency-Cached: true`) instead of writing again. Error responses are not replayed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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."
                  },
                  "template": {
                    "type": "string",
                    "description": "Build the workspace from this template — its `slug` or numeric `id` from `GET /workspace/templates`. A slug is looked up among the system templates first, then the tenant's own; an all-digit value that matches no slug is read as an id. An unknown template is `422` (\"Template <ref> not found.\"). Must be a single value — an array or object is `422` (\"template must be a single id.\"). A workspace built from a template is created as a DRAFT (`status: draft`): only its owners can see it, nobody is notified, and its to-dos wait on their roles until `POST /workspace/workspaces/{id}/launch`.",
                    "example": "customer-onboarding"
                  },
                  "draft": {
                    "type": "boolean",
                    "description": "Blank workspaces only: create it as a draft — owners-only and silent until launched. A workspace from a template is always a draft."
                  },
                  "template_slug": {
                    "type": "string",
                    "description": "Alias for `template`, used only when `template` is absent. Must be a single value — an array or object is `422` (\"template_slug must be a single id.\")."
                  },
                  "template_id": {
                    "type": "string",
                    "description": "Alias for `template`, used only when neither `template` nor `template_slug` is sent. Accepts an id or a slug. Must be a single value — an array or object is `422` (\"template_id must be a single id.\")."
                  },
                  "anchor_date": {
                    "type": "string",
                    "format": "date",
                    "description": "For a dated template (`setup.scheduled`): the date its plan counts from — go-live, opening day, the deadline (`setup.anchor_label`). Each to-do and milestone is dated from it; a negative offset counts back. Omitted, the plan is created with no dates and a warning says so. An unreadable date is `422`.",
                    "example": "2027-03-01"
                  },
                  "roles": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Who holds each of the template's role slots, as role `key` (from `setup.roles`) → user id. The person joins the workspace and owns that role's to-dos, milestones and risks. A key sent empty leaves the slot open; a key not sent falls back to the slot's default (`defaults_to_creator`: the caller). An open slot's to-dos stay unassigned with `unfilled_role` naming it, and a warning lists it.",
                    "example": {
                      "site_manager": "4211",
                      "it_lead": ""
                    }
                  },
                  "kind": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "store",
                      "district",
                      "region",
                      "team",
                      "project",
                      "client_account",
                      "committee",
                      "training_cohort",
                      "mentoring",
                      "social",
                      "legal_matter",
                      "tracker",
                      "event",
                      "release",
                      "other",
                      null
                    ],
                    "description": "What sort of workspace this is. With `template`, defaults to the template's `kind`; a kind you send wins. Blank is ignored. Any other value is `422`."
                  },
                  "tags": {
                    "description": "Free-form tags, as a JSON array of strings or one comma-separated string. Blank entries are dropped and duplicates removed case-insensitively. At most 20 tags, each 40 characters or fewer; more is `422` rather than a silent truncation.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "example": [
                      "onboarding",
                      "q3"
                    ]
                  },
                  "ends_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The date the workspace's work ends (YYYY-MM-DD), which drives the close-out prompt. A value that is not a date between the years 1900 and 9999 is `422`.",
                    "example": "2026-12-31"
                  },
                  "series_cadence": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "yearly",
                      "quarterly",
                      "monthly",
                      null
                    ],
                    "description": "For a workspace that recurs as a series, how often. Any other value is `422`."
                  },
                  "starts_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The date the project starts (YYYY-MM-DD). Not after `ends_on`; a value that is not a date between the years 1900 and 9999 is `422`.",
                    "example": "2026-10-01"
                  },
                  "health": {
                    "type": "string",
                    "enum": [
                      "on_track",
                      "at_risk",
                      "off_track",
                      "not_set"
                    ],
                    "description": "The owner's own rating of how the project is going. Defaults to `not_set`; any other value is `422`."
                  },
                  "organizational_department_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The department the project belongs to — an active department of this business, else `422`."
                  },
                  "sponsor_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The project's sponsor (the accountable leader) — an active person in this business, else `422`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace"
                  ],
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present only when the workspace was created from a `template` and some of the template's content could not be applied (for example its welcome message could not be posted). The workspace exists; each entry says what it lacks and where to set it up."
                    }
                  }
                }
              }
            }
          },
          "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`). The attributes are invalid (blank name, name already taken, a `kind`, `series_cadence`, `tags` or `ends_on` outside the accepted values), the `template` names no template visible to this business (\"Template <ref> not found.\"), `template` / `template_slug` / `template_id` was an array or object (\"<key> must be a single id.\"), or the tenant's `create_workspace` policy does not admit the caller. `error.details` carries the individual messages when the service refused.",
            "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. Carries\n`health_signals` beside the owner's own `health` (PM-3): what the data\nshows.\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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Workspace"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "health_signals": {
                              "$ref": "#/components/schemas/WorkspaceHealthSignals"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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, and\nreclassify (`kind`, `tags`) or reschedule (`ends_on`,\n`series_cadence`).\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. For `kind`,\n`tags`, `ends_on` and `series_cadence`, sending the key with a blank\nvalue (`null` or `\"\"`) CLEARS it. Send at least one updatable key, or\nthe request is refused with `422`.\n\n`kind`, `tags`, `ends_on` and `series_cadence` are checked by the same\nrules as on create; a value outside them is `422` naming the accepted\nvalues.\n\n`PUT` is routed to the same action and behaves identically.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours. A retry carrying the same key replays the original response (header `X-Idempotency-Cached: true`) instead of writing again. Error responses are not replayed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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
                  },
                  "kind": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "store",
                      "district",
                      "region",
                      "team",
                      "project",
                      "client_account",
                      "committee",
                      "training_cohort",
                      "mentoring",
                      "social",
                      "legal_matter",
                      "tracker",
                      "event",
                      "release",
                      "other",
                      null
                    ],
                    "description": "What sort of workspace this is. Blank clears it. Any other value is `422`."
                  },
                  "tags": {
                    "description": "REPLACES the workspace's tags: a JSON array of strings or one comma-separated string. Blank entries are dropped and duplicates removed case-insensitively; a blank value or an empty list clears every tag. At most 20 tags, each 40 characters or fewer; more is `422` rather than a silent truncation.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "nullable": true
                  },
                  "ends_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The date the workspace's work ends (YYYY-MM-DD). Blank clears it. A value that is not a date between the years 1900 and 9999 is `422`."
                  },
                  "series_cadence": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "yearly",
                      "quarterly",
                      "monthly",
                      null
                    ],
                    "description": "Blank clears it. Any other value is `422`."
                  },
                  "starts_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The date the project starts (YYYY-MM-DD). Blank clears it. A start after the end date, or a value that is not a date between the years 1900 and 9999, is `422`."
                  },
                  "health": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "on_track",
                      "at_risk",
                      "off_track",
                      "not_set",
                      null
                    ],
                    "description": "The owner's own rating of how the project is going. Blank resets it to `not_set`; any other value is `422`."
                  },
                  "organizational_department_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The department the project belongs to — an active department of this business, else `422`. Blank clears it."
                  },
                  "sponsor_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The project's sponsor — an active person in this business, else `422`. Blank clears it."
                  }
                }
              }
            }
          }
        },
        "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 (error code `validation_failed`) — the body carried none of the updatable keys (\"Supply at least one of name, description, icon, color, kind, tags, ends_on, series_cadence, starts_on, health, organizational_department_id, sponsor_id.\"), `name` / `description` / `icon` / `color` was not a string, a `kind`, `series_cadence`, `tags`, `ends_on`, `starts_on` or `health` value is outside the accepted values, the department or sponsor is not an active one of this business, or the attributes are otherwise invalid (including a content-moderation refusal of the name or description).",
            "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 external 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 external access\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 the external digest is turned off 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"
                }
              }
            }
          },
          "500": {
            "description": "The digest could not be generated (error code `digest_failed`, \"Couldn't generate the external digest. Please try again.\"). This is a server-side failure, not a problem with the request — retrying the same call is the right response. 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/{id}/leave": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Leave a workspace",
        "description": "Takes the token's user out of the workspace — only their own\nmembership; there is no way to aim it at anyone else (removing another\nmember is an owner action on the web roster). Their open tasks in the\nworkspace become unassigned, and the change is recorded in the audit\ntrail like every membership change.\n\nRefused when:\n* they are the workspace's only owner — someone else must be made an\n  owner first (`409 last_owner`);\n* the workspace's access rule admits them — people a rule includes\n  cannot be removed one by one; they can mute the workspace in the app\n  instead, or an owner can change the rule (`409 access_rule`);\n* they have no membership to leave (`422 not_a_member`);\n* the workspace is archived (`403 forbidden`).\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:workspace`\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller left the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "left",
                    "workspace_id",
                    "role",
                    "unassigned_task_count"
                  ],
                  "properties": {
                    "left": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "workspace_id": {
                      "type": "integer"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "owner",
                        "member",
                        "viewer"
                      ],
                      "description": "The role they held."
                    },
                    "unassigned_task_count": {
                      "type": "integer",
                      "description": "How many of their open tasks in the workspace were unassigned."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The workspace is archived (error code `forbidden`), or the token lacks `destructive:workspace` (error code `insufficient_permissions`).",
            "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 caller cannot leave: they are the only owner (error code `last_owner`), or the access rule admits them (error code `access_rule`, with `details.rule_name`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The caller has no membership in this workspace to leave (error code `not_a_member`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{id}/launch": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          },
          "example": "42"
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Launch a draft workspace",
        "description": "Makes a draft live. Until now only its owners could see it and nobody\nwas notified. Launching tells every member they were added, assigns each\ntemplate to-do to its role's holder (one notice per holder), starts the\ncheck-ins on their schedule and announces the welcome post.\nOwners only (`403 forbidden`). Launching a workspace that is already live\nsucceeds with `launched: false`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "roles": {
                    "type": "object",
                    "description": "Optional `{ role_key: user_id }` — change who holds a template role before anyone is told. A holder replaced who was only in the workspace for that role is taken back out.",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Launched (or already live).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace": {
                      "type": "object"
                    },
                    "launched": {
                      "type": "boolean"
                    },
                    "to_dos_assigned": {
                      "type": "integer"
                    },
                    "members_notified": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an owner of the workspace."
          },
          "422": {
            "description": "A picked role holder isn't an active member of the organization, or roles isn't an object."
          }
        }
      }
    },
    "/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\n**Attach files with the post.** Send the body as `multipart/form-data`\nwith `attachments[]` file parts (at most 10) alongside the fields. The\nbytes enter through Rails, are pushed to storage concurrently and land\nthrough the same Drive pipeline as the web composer — one request, no\nupload URLs, nothing to clean up. This replaces the deprecated\nprepare/upload/commit publication flow. A refused file (over its cap —\n10 MB, or 100 MB when the REAL bytes are video) fails the call with\n`422` and nothing is posted or stored. On an `external_id` replay the\nfiles are matched by byte checksum, so a retried post never doubles its\nattachments. Failures from the storage layer AFTER the post exists are\nreported as `attachment_errors` rather than losing the post.\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 external digest and external access. 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."
                  },
                  "visible_to": {
                    "type": "string",
                    "enum": [
                      "members",
                      "owners"
                    ],
                    "description": "`members` (everyone, the default) or `owners` (leads only). `owners` from a caller who is neither the workspace owner nor a Workspace app admin is `403` rather than a silent downgrade; any other value is `422` (\"visible_to must be one of: members, owners.\")."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "announcement"
                    ],
                    "default": "standard",
                    "description": "`announcement` posts a banner announcement, which reaches members who muted ordinary posts. Only the workspace owner or a Workspace app admin; anyone else is `403` (\"Only the workspace owner or a Workspace admin can post an announcement.\")."
                  },
                  "cascade_to_locations": {
                    "type": "boolean",
                    "default": false,
                    "description": "Also post a copy into every location workspace beneath this workspace's location (a district's stores). Only the workspace owner or a Workspace app admin (`403` otherwise), never with `visible_to: owners` (`422`). Each copy carries the title and body; attachments stay on this post. The response's `cascade` object counts the copies that `landed`, those `held_for_review` by a store's owner, and the stores `not_posted` (a store whose message board is off, one that removed or declined an earlier copy, or any past the first 100)."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "body"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "internal_only": {
                    "type": "boolean",
                    "default": false
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "visible_to": {
                    "type": "string",
                    "enum": [
                      "members",
                      "owners"
                    ]
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "announcement"
                    ]
                  },
                  "cascade_to_locations": {
                    "type": "boolean",
                    "default": false
                  },
                  "attachments": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "File parts posted WITH the message."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing message replayed for the same `external_id` and identical content.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "replayed",
                    "pending_review"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "True — this is the post an earlier call with the same `external_id` created. The `X-External-Id-Replayed: true` header says the same."
                    },
                    "pending_review": {
                      "type": "boolean",
                      "description": "True when the existing post is awaiting review under the workspace's posting policy (`message.review_status` is `pending_review`) — it is not yet visible to the roster. A replayed post that was `rejected` answers false here; read `message.review_status` for the full state."
                    },
                    "sent_back_for_review": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present (and true) only when this replay attached a NEW file to a post that was already live, and that sent the post back for review under the posting policy. Absent otherwise."
                    },
                    "attachment_errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present only when a file could not be stored; the post itself landed."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The created message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "replayed",
                    "pending_review"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        false
                      ],
                      "description": "False — a new post was created."
                    },
                    "pending_review": {
                      "type": "boolean",
                      "description": "True when the workspace's posting policy is holding the new post for review (`message.review_status` is `pending_review`): the `201` means it was saved, not that the roster can see it yet. It goes live when the owner or a Workspace app admin approves it."
                    },
                    "attachment_errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present only when a file could not be stored; the post itself landed."
                    }
                  }
                }
              }
            }
          },
          "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 `visible_to: owners` was sent by a caller who 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"
                }
              }
            }
          },
          "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 PAGED, oldest comment first (by `created_at`, then `id`,\nso a tie never straddles a page boundary twice). A call returns one\npage — `comment_per_page` comments (default 25, maximum 100) of page\n`comment_page` (default 1) — and `message.comments_meta` describes\nwhere that page sits, in the same shape as the `meta` block on every\ncollection here. To walk the whole thread, repeat the call with\n`comment_page` + 1 while `comments_meta.has_next_page` is true; the\nnewest replies are on page `comments_meta.total_pages`.\n`comment_count` on the message is the thread's true total.\n\nA comment posted through Workspace external access 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_page",
            "in": "query",
            "required": false,
            "description": "Which page of the comment thread to return, 1-based. A blank or non-numeric value returns page 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "comment_per_page",
            "in": "query",
            "required": false,
            "description": "Comments per page. Maximum 100; a larger value is clamped to it, and a blank or non-numeric value falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "comment_limit",
            "in": "query",
            "required": false,
            "description": "Alias for `comment_per_page`, kept because it is what this endpoint shipped with. `comment_per_page` wins when both are sent.",
            "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",
                              "description": "One page of the thread, oldest first.",
                              "items": {
                                "$ref": "#/components/schemas/WorkspaceMessageComment"
                              }
                            },
                            "comments_meta": {
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/WorkspacePaginationMeta"
                                }
                              ],
                              "description": "Pagination state for `comments`, scoped to this thread. Unlike a collection's `meta`, it is NOT mirrored in the `X-*` pagination headers."
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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`, `visible_to`, or the request is refused with `422`\n(\"Supply at least one of title, body, internal_only, visible_to.\").\n\n`visible_to` moves the post between everyone (`members`) and leads\nonly (`owners`). Only the workspace owner or a Workspace app admin can\nchange it: `owners` from anyone else is `403`, and `members` from\nanyone else is accepted but leaves the stored value unchanged. Any\nother value is `422`.\n\nUnder a posting policy that holds member posts for review, an edit that\nchanges the `title` or `body` of a non-reviewer's post sends it back\nfor review (`review_status: pending_review`) and clears any\n`review_notes`; reviewers' own edits never re-enter review.\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",
                    "description": "Must be a string — an array or object is `422` (\"title must be a string.\")."
                  },
                  "body": {
                    "type": "string",
                    "description": "Must be a string — an array or object is `422` (\"body must be a string.\"). `@mentions` are re-extracted."
                  },
                  "internal_only": {
                    "type": "boolean",
                    "description": "Null is read as `false`."
                  },
                  "visible_to": {
                    "type": "string",
                    "enum": [
                      "members",
                      "owners"
                    ],
                    "description": "`members` (everyone in the workspace) or `owners` (leads only). `owners` from a caller who is neither the workspace owner nor a Workspace app admin is `403` (\"Only the workspace owner (or a Workspace app admin) can post to leads only.\"); from such a caller `members` is accepted but not applied. Any other value is `422` (\"visible_to must be one of: members, owners.\"), as is a non-string (\"visible_to must be a string.\")."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "announcement"
                    ],
                    "description": "Marks the post an announcement, or un-marks one. Changing it is the workspace owner's or a Workspace app admin's call (`403` otherwise); a meeting note or status update keeps its kind (`422`)."
                  }
                }
              }
            }
          }
        },
        "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, the caller is neither the message's author nor a workspace admin, or `visible_to: owners` was sent by a caller who is neither the workspace owner nor a Workspace app 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, `title` / `body` / `visible_to` was not a string, `visible_to` was neither `members` nor `owners`, or the values are invalid (including a content-moderation refusal).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{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": "id",
          "in": "path",
          "required": true,
          "description": "The message id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Approve a held post",
        "description": "Publishes a post the workspace's posting policy is holding for review\n(`review_status: pending_review`). The post goes live, the workspace\nroster is notified of it — once per post: a post that was approved\nbefore, edited and re-held, is not announced to the roster a second\ntime, though anyone the edit newly @-mentions is told — and the author\nis told it was approved. Any earlier `review_notes` are cleared.\n\n**Reviewers only** — the workspace owner or a Workspace app admin.\nAnyone else, including the post's author and other contributors, gets\n`403`. The workspace must not be archived.\n\nRequires the business-level `enable_message_board` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The approved post, now live",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Nothing was decided (error code `forbidden`). The app or the message board is disabled, the caller is not a member, the workspace is archived, or the caller is neither the workspace owner nor a Workspace app admin (\"Only the workspace owner or a Workspace admin can review posts.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or no message with that id the caller can read (a leads-only post, or someone else's held post, is `404` to anyone who could not open it) — error code `not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was decided (error code `validation_failed`, the reason in `error.message` and `error.details`) — most often \"This post is not awaiting review.\" for a post that is already published or was rejected; rarely, the post's own validation messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{id}/reject": {
      "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"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Reject a held post",
        "description": "Turns down a post the workspace's posting policy is holding for review\n(`review_status: pending_review`). The post is NOT deleted: it moves to\n`rejected`, stays readable by its author and the reviewers only, and\ncarries the reason in `review_notes`. The author is notified, and\nrevising the post sends it back for review.\n\n**Reviewers only** — the workspace owner or a Workspace app admin.\nAnyone else, including the post's author and other contributors, gets\n`403`. The workspace must not be archived.\n\nRequires the business-level `enable_message_board` toggle.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:workspace` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "review_notes"
                ],
                "properties": {
                  "review_notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "The reason, shown to the author so they can revise the post. Required: blank or missing is `422` (\"Give the author a reason so they can revise the post.\"), and so is a non-string value. Leading and trailing whitespace is trimmed and CRLF counts as one character; more than 1,000 characters is `422` (\"Keep the reason to 1,000 characters or fewer.\").",
                    "example": "Please add the store hours before this goes out."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rejected post",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Nothing was decided (error code `forbidden`). The app or the message board is disabled, the caller is not a member, the workspace is archived, or the caller is neither the workspace owner nor a Workspace app admin (\"Only the workspace owner or a Workspace admin can review posts.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or no message with that id the caller can read (a leads-only post, or someone else's held post, is `404` to anyone who could not open it) — error code `not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was decided (error code `validation_failed`, the reason in `error.message` and `error.details`): \"This post is not awaiting review.\", \"Give the author a reason so they can revise the post.\", \"Keep the reason to 1,000 characters or fewer.\", or — rarely — the post's own validation messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/messages/{id}/mark-reviewed": {
      "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"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Mark a post as still accurate",
        "description": "Records that someone checked the post and it is still right — the same\n\"Still accurate\" action the post's page offers. Content governance\nnudges a workspace's owners weekly about posts it judges stale, and an\nowner may not edit a member's post; this is how they clear the nudge\nwithout deleting the post.\n\nSets `last_reviewed_at` to now and nothing else: `updated_at` does not\nmove, the post is not re-sent for review and nobody is notified. The\npost's open stale-content governance findings are resolved at once, and\nthe next scan ages the post from this check. Calling it again simply\nmoves the date.\n\n**Who may call it:** the post's author, any owner of the workspace, or\na Workspace app admin. Only a post on the board (`review_status:\npublished`) can be marked; the workspace must not be archived.\n\nRequires the business-level `enable_message_board` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The post, with `last_reviewed_at` set to now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Nothing was recorded (error code `forbidden`). The app or the message board is disabled, the caller is not a member, the workspace is archived, or the caller is not the author, a workspace owner or a Workspace app admin (\"Only the author, a workspace owner or a Workspace admin can mark this post as still accurate.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such workspace, or no message with that id the caller can read (a leads-only post, or someone else's held post, is `404` to anyone who could not open it) — error code `not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was recorded (error code `validation_failed`): the post is not on the board — held for review or not approved (\"Only a post on the board can be marked as still accurate.\").",
            "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 files to a message",
        "description": "Adds files 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` with `attachments[]` file parts\n(at most 10 per request); a single `file` part is the older shape and\nstill works. The bytes are pushed to storage CONCURRENTLY before\nanything is attached, so several files cost one round trip rather than\none each.\n\nOrdinary files may be up to 10 MB; files whose REAL bytes are video may\nbe up to 100 MB — the cap is applied to the sniffed media type as well\nas the declared one, so an executable renamed `.mp4` is refused (`422`)\nand its bytes are purged in the same request. Refusals are\nall-or-nothing: nothing is attached.\n\nReplay is per file, by byte checksum: a request whose files are all\nalready attached returns `200` with `replayed: true` and the\n`X-Attachment-Replayed` header, instead of storing duplicates.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "attachments": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The file parts. Use `file` for a single upload."
                  },
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every file in the request was already attached; the existing attachments were replayed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attachment",
                    "attachments",
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "attachment": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                        }
                      ],
                      "description": "The FIRST attachment, for single-file clients."
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                      }
                    },
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Files attached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attachment",
                    "attachments",
                    "message",
                    "replayed"
                  ],
                  "properties": {
                    "attachment": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                        }
                      ],
                      "description": "The FIRST attachment, for single-file clients."
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceMessageAttachment"
                      }
                    },
                    "message": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "replayed": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "attachment_errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present only when some file could not be stored."
                    }
                  }
                }
              }
            }
          },
          "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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:workspace` scope.\n",
        "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 — pending, in progress or overdue; excludes completed and cancelled), `completed`, `cancelled`, `overdue` (past due and neither completed nor cancelled), or `all`. An unrecognised value behaves as `active`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "completed",
                "cancelled",
                "overdue",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "description": "Only tasks assigned to this user. Must be a single numeric id — an array or object is `422` (`validation_failed`, \"assignee_id must be a single id.\"), and a value that is not all digits (`abc`, `12abc`) is `422` (\"assignee_id must be a numeric id.\") rather than a filter that silently matches nothing or someone else.",
            "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"
                }
              }
            }
          },
          "422": {
            "description": "`assignee_id` was an array or object rather than a single id (error code `validation_failed`, \"assignee_id must be a single id.\"), or not all digits (\"assignee_id must be a numeric id.\").",
            "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": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours. A retry carrying the same key replays the original response (header `X-Idempotency-Cached: true`) instead of writing again. Error responses are not replayed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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. Must be a single id — an array or object is `422` (\"assignee_id must be a single id.\")."
                  },
                  "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. Must be a single id — an array or object is `422` (\"task_list_id must be a single id.\")."
                  },
                  "list_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Alias for `task_list_id`. Must be a single id — an array or object is `422` (\"list_id must be a single id.\")."
                  },
                  "internal_only": {
                    "type": "boolean",
                    "default": false,
                    "description": "Excludes the task from the external digest and external access."
                  },
                  "recurring_cadence": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "daily",
                      "weekly",
                      "monthly",
                      null
                    ],
                    "description": "Repeat the task on this cadence. Refused with `422` while recurring to-dos are turned off for the workspace."
                  },
                  "parent_task_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Create a SUBTASK under this top-level to-do of the workspace (a milestone's deliverable). A subtask has no list (`list_id` is null) and cannot have subtasks of its own; at most 100 per to-do. An id not in this workspace is `404`."
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The day work starts (PM-1). A date, or null to clear it. It may not be after the due date (`422`)."
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "normal",
                      "important",
                      "critical"
                    ],
                    "description": "Blank or null sets `normal`."
                  },
                  "estimated_duration_minutes": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "description": "The estimate, in minutes. Null clears it."
                  },
                  "milestone": {
                    "type": "boolean",
                    "description": "A milestone (PM-3) — a point in the plan; its subtasks are its deliverables. A subtask cannot be a milestone."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "This workspace's custom to-do fields as `field_name` (or the definition id) → value, checked against each field's type (see `GET …/custom-fields`). A value that fails its type is `422`. Fields you leave out keep their value."
                  },
                  "collaborator_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "The collaborators beside the one accountable owner (`assignee_id`) — active members of the workspace, at most 20. Replaces the list; `[]` clears it."
                  }
                }
              }
            }
          }
        },
        "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, a `recurring_cadence` while recurring to-dos are turned off, an array or object where `task_list_id`, `list_id` or `assignee_id` expects a single id (\"<key> must be a single id.\"), a value there that is not all digits (\"<key> must be a numeric id.\"), 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": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry key retained by the platform for 24 hours. A retry carrying the same key replays the original response (header `X-Idempotency-Cached: true`) instead of writing again. Error responses are not replayed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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`. Must be a single id — an array or object is `422` (\"assignee_id must be a single id.\")."
                  },
                  "due_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "ISO 8601 datetime. Null clears the due date."
                  },
                  "recurring_cadence": {
                    "type": "string",
                    "nullable": true,
                    "description": "`daily`, `weekly`, `monthly`, or null to stop repeating. Changing it while recurring to-dos are turned off is `422`; re-sending the stored value is accepted."
                  },
                  "internal_only": {
                    "type": "boolean"
                  },
                  "task_list_id": {
                    "type": "integer",
                    "description": "A task list in this workspace. `list_id` is accepted as an alias. Must be a single id — an array or object is `422` (\"task_list_id must be a single id.\")."
                  },
                  "list_id": {
                    "type": "integer",
                    "description": "Alias for `task_list_id`. Must be a single id — an array or object is `422` (\"list_id must be a single id.\")."
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "The day work starts (PM-1). A date, or null to clear it. It may not be after the due date (`422`)."
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "normal",
                      "important",
                      "critical"
                    ],
                    "description": "Blank or null sets `normal`."
                  },
                  "estimated_duration_minutes": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "description": "The estimate, in minutes. Null clears it."
                  },
                  "milestone": {
                    "type": "boolean",
                    "description": "A milestone (PM-3) — a point in the plan; its subtasks are its deliverables. A subtask cannot be a milestone."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "This workspace's custom to-do fields as `field_name` (or the definition id) → value, checked against each field's type (see `GET …/custom-fields`). A value that fails its type is `422`. Fields you leave out keep their value."
                  },
                  "collaborator_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "The collaborators beside the one accountable owner (`assignee_id`) — active members of the workspace, at most 20. Replaces the list; `[]` clears it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated task",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "task"
                  ],
                  "properties": {
                    "task": {
                      "$ref": "#/components/schemas/WorkspaceTask"
                    },
                    "schedule_conflicts": {
                      "type": "array",
                      "description": "The finish-to-start links (PM-2a) this to-do now clashes with — it starts before a to-do it waits on finishes, or a to-do waiting on it does. Empty when none. Shift them with `POST …/schedule` (`mode: shift`).",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceScheduleConflict"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The app or Tasks is disabled, the caller is not a member, the caller is a read-only viewer, or the caller is neither a workspace owner nor a Workspace app admin and the request lifts `internal_only` (true → false), which shares the to-do with external guests (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, `recurring_cadence` was changed while recurring to-dos are turned off, an array or object was sent where `task_list_id`, `list_id` or `assignee_id` expects a single id (\"<key> must be a single id.\"), a value there that is not all digits (\"<key> must be a numeric id.\"), `internal_only` was changed while external access and the external digest are both off, 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. For contributors, and for the task's own\nassignee whatever their workspace role (a read-only viewer may complete\na task assigned to them). Idempotent enough to retry: the response\nalways 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 who is not the task's assignee (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. For contributors, and\nfor the task's own assignee whatever their workspace role.\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 who is not the task's assignee (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}/custom-fields": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "The project's custom to-do fields",
        "description": "This workspace's custom to-do fields (PM-1), in their order. A to-do\ncarries their values under `custom_fields` (`field_name` → value), and\ncreate / update take the same shape. Members read; fields are managed on\nthe web. Requires the `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "custom_fields",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "custom_fields": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceCustomField"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a member, or Tasks is switched off (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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/risks": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "The project's risks",
        "description": "The project's risks (PM-3, decision D16) — Compliance Hub register rows\nsourced on this workspace, rated on a 5×5 likelihood × impact scale.\nOpen ones by default (not accepted or closed); `status=all` for every\none. Highest score first. Members read.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "all"
              ],
              "default": "open"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The risks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "risks",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "risks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceRisk"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a member (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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Raise a risk",
        "description": "Raise a risk on the project. CONTRIBUTORS ONLY. Where Compliance Hub is\nenabled it also appears in the organization's risk register. There is no\ndelete: a risk ends by closing it (`PATCH` with `status: closed`).\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": {
                "$ref": "#/components/schemas/WorkspaceRiskInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The risk",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "risk"
                  ],
                  "properties": {
                    "risk": {
                      "$ref": "#/components/schemas/WorkspaceRisk"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A read-only viewer, or the workspace is archived (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was saved (error code `validation_failed`) — a rating outside 1–5, an unknown category or status, an owner who is not an active member of the project, or text the content screen refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/risks/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Workspace"
        ],
        "summary": "Change a risk",
        "description": "Only the keys you send are written. The project's owners, a Workspace\nadmin, whoever raised the risk and the risk's owner may change it\n(anyone else is `403`). Close it with `status: closed`. `PUT` behaves\nidentically.\n\n**Token scopes:** `write:workspace`; ending the risk — `status: closed`\nor `status: accepted`, its two final statuses — also asks\n`destructive:workspace` (#7248).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkspaceRiskInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The risk",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "risk"
                  ],
                  "properties": {
                    "risk": {
                      "$ref": "#/components/schemas/WorkspaceRisk"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A viewer, not someone who may change this risk, or the workspace is archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such risk on this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A value was refused, or nothing was sent (error code `validation_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/status-updates": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Post a project status update",
        "description": "An OWNER's project status update (PM-3): posted to the message board as\na message of kind `status_update`, it sets the workspace's `health` when\n`health` is given and answers the owner-only status-update reminder.\nInternal-only by default: external access or the external digest never shows it\nunless `share_with_client` is true. Owners and Workspace admins only; the\nmessage board must be on.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "summary"
                ],
                "properties": {
                  "summary": {
                    "type": "string",
                    "maxLength": 10000,
                    "description": "How the project is going and what the team should know."
                  },
                  "health": {
                    "type": "string",
                    "enum": [
                      "on_track",
                      "at_risk",
                      "off_track"
                    ]
                  },
                  "share_with_client": {
                    "type": "boolean",
                    "default": false,
                    "description": "Also show the update to the workspace's external guests (external access and the external digest). Honoured only where an external guest can read the board; otherwise the update stays internal-only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The status update",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status_update"
                  ],
                  "properties": {
                    "status_update": {
                      "$ref": "#/components/schemas/WorkspaceMessage"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an owner, the board is off, or the workspace is archived (error code `forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A blank or too-long summary, an unknown health, or refused text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/dependencies": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "The project's finish-to-start links",
        "description": "Every link between this workspace's to-dos (PM-2a): the successor may\nstart `lag_days` after its predecessor finishes (a negative lag lets\nthem overlap). `task_id` narrows to the links touching one to-do (either\nend). Members read. Requires the `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "task_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dependencies",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "dependencies": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceDependency"
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a member, or Tasks is switched off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`task_id` was not a single numeric id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Make a to-do wait on another",
        "description": "CONTRIBUTORS ONLY. `successor_id` will wait on `predecessor_id` — both\ntop-level to-dos of this workspace (a subtask follows its parent). A\nloop is refused, as is a to-do waiting on more than 20 others. Sending a\npair that is already linked changes its `lag_days` (`201` either way).\n`warnings` names what now starts too early; nothing is moved — shift\nwith `POST …/schedule`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "successor_id",
                  "predecessor_id"
                ],
                "properties": {
                  "successor_id": {
                    "type": "integer",
                    "description": "The to-do that waits."
                  },
                  "predecessor_id": {
                    "type": "integer",
                    "description": "The to-do it waits on."
                  },
                  "lag_days": {
                    "type": "integer",
                    "minimum": -365,
                    "maximum": 365,
                    "default": 0,
                    "description": "Days after the predecessor finishes before the successor may start."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dependency",
                    "warnings"
                  ],
                  "properties": {
                    "dependency": {
                      "$ref": "#/components/schemas/WorkspaceDependency"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A viewer, Tasks is off, or the workspace is archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`successor_id` or `predecessor_id` is not a to-do in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Refused (error code `validation_failed`) — a loop, a subtask, a to-do waiting on itself, a lag outside -365..365, or too many links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/dependencies/{id}": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Workspace"
        ],
        "summary": "Change a link's gap",
        "description": "CONTRIBUTORS ONLY. `lag_days` is required. `PUT` behaves identically.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "lag_days"
                ],
                "properties": {
                  "lag_days": {
                    "type": "integer",
                    "minimum": -365,
                    "maximum": 365
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dependency",
                    "warnings"
                  ],
                  "properties": {
                    "dependency": {
                      "$ref": "#/components/schemas/WorkspaceDependency"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A viewer, Tasks is off, or the workspace is archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such link in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A missing or out-of-range `lag_days`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Workspace"
        ],
        "summary": "Stop a to-do waiting on another",
        "description": "CONTRIBUTORS ONLY. Dates are not changed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A viewer, Tasks is off, or the workspace is archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such link in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/timeline": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Workspace"
        ],
        "summary": "The project's timeline",
        "description": "The desktop Timeline tab's data (PM-2a): the workspace's TOP-LEVEL to-dos\nthat have a start or due date, done ones included and cancelled ones\nleft out, in timeline order (first day, then due, then id) — each with\nits span (`starts_on`..`ends_on`, the days the calendar gives it; a\nmilestone is its due day) and what it waits on. `dependencies` are the\nlinks among this page's rows; `conflicts` are the workspace's to-dos\nthat start before what they wait on finishes; `undated_count` counts\nthe open to-dos with no date. Members read; not gated on the plan.\nRequires the `enable_tasks` toggle.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The timeline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tasks",
                    "dependencies",
                    "conflicts",
                    "undated_count",
                    "total_count",
                    "meta"
                  ],
                  "properties": {
                    "tasks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceTimelineTask"
                      }
                    },
                    "dependencies": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceDependency"
                      }
                    },
                    "conflicts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceScheduleConflict"
                      }
                    },
                    "undated_count": {
                      "type": "integer"
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/WorkspacePaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a member, or Tasks is switched off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/workspaces/{workspace_id}/schedule": {
      "parameters": [
        {
          "name": "workspace_id",
          "in": "path",
          "required": true,
          "description": "The workspace's numeric id OR its slug.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Workspace"
        ],
        "summary": "Preview or shift late to-dos",
        "description": "`mode: preview` (the default) returns the moves a shift would make and\nwrites nothing. `mode: shift` makes them — CONTRIBUTORS ONLY: every to-do\nthat starts before what it waits on finishes moves LATER, whole (start\nand due by the same days), in dependency order so a move cascades down\nthe chain; all the moves or none. Nothing is ever moved earlier, and a\nfinished, cancelled or already-started to-do is not moved. A moved\nto-do's open, not-started, dated subtasks move with it by the same days\n(each move's `subtasks`). Both modes need the `write:workspace` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "preview",
                      "shift"
                    ],
                    "default": "preview"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The moves (previewed or made) and the conflicts left",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "mode",
                    "moves",
                    "conflicts"
                  ],
                  "properties": {
                    "mode": {
                      "type": "string",
                      "enum": [
                        "preview",
                        "shift"
                      ]
                    },
                    "moves": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceScheduleMove"
                      }
                    },
                    "conflicts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceScheduleConflict"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a member, Tasks is off, or (to shift) a viewer or an archived workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "An unknown `mode`, or a move was refused (nothing was moved).",
            "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. Only check-ins that take a written\nanswer are drafted; one that asks for a typed answer (`answer_type`\nother than `text`) never appears here.\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"
                        },
                        "answer_value": {
                          "type": "string",
                          "nullable": true,
                          "description": "A typed answer's value — null for a draft, which is always a written answer."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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 (error code `validation_failed`). `error.message` carries the service's own reason(s), joined, and `error.details` the individual messages — read them verbatim; most are the caller's to act on: an empty response (\"Your response can't be blank. Edit the draft or delete it instead of approving an empty response.\"), a content-moderation refusal of `response_text`, a record validation such as \"Response text is too long (maximum is 10000 characters)\", check-ins turned off for this specific workspace, a draft whose check-in has since started asking for a typed answer, or a non-string `response_text` (\"response_text must be a string.\", with no `details`). An internal fault answers the fixed line \"Couldn't approve this check-in response. Please try again.\" — the detail is logged server-side, and that one is worth retrying.",
            "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. Only a recognised false value — `false`, `0`, `\"false\"`, `\"0\"`, `\"f\"` or `\"off\"` (or their all-upper-case spellings) — selects past events; any other value, including an unrecognised string, behaves as the default (upcoming).",
            "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": "Adds or removes the caller's reaction with ONE emoji on one item.\n`reacted` in the response is `true` when the reaction is now ON,\n`false` when it is off; `action` says the same as `added` / `removed`,\nand `counts` returns the item's full per-emoji tally so a client can\nre-render without a second call. CONTRIBUTORS ONLY.\n\n- body `{ reactable_type, reactable_id, emoji }` → TOGGLE: adds the\n  reaction if absent, removes it if present.\n- body `{ reactable_type, reactable_id, emoji, reacted: true|false }`\n  → SET the reaction to that exact state. `true` ensures the caller's\n  reaction with that emoji exists (no change if it already does);\n  `false` ensures it does not (no change if it already doesn't).\n\n**Send `reacted` to make the call safe to retry.** A bare toggle is\nNOT retry-safe: the server adds the reaction, the response is lost,\nthe client retries, and the retry removes what was just added. This\nendpoint does not honour `Idempotency-Key`; a request that states the\nstate it wants is idempotent by construction. Same contract as\n`POST /broadcasts/{broadcast_id}/reactions/toggle`.\n\nThe emoji must be in the item's allowed set; anything else is `422`.\nPosts, replies and to-dos offer the company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed).\nA reaction the caller already holds from outside it is still accepted,\nso it can be removed.\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": "👍"
                  },
                  "reacted": {
                    "type": "boolean",
                    "description": "Optional: the state you want. `true` → the caller's reaction with this emoji is ON afterwards; `false` → it is OFF. Already in that state is a no-op that still answers `200`, so a retry is safe. Cast as a boolean, so JSON booleans and the usual string spellings (`\"true\"`, `\"false\"`, `\"1\"`, `\"0\"`) work. Absent, `null` or `\"\"` → toggle, which is NOT safe to retry."
                  },
                  "on": {
                    "type": "boolean",
                    "description": "Alias for `reacted`, read only when `reacted` is absent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The toggle result and the item's new tally",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "action",
                    "reacted",
                    "counts"
                  ],
                  "properties": {
                    "action": {
                      "type": "string",
                      "enum": [
                        "added",
                        "removed"
                      ],
                      "description": "`added` when the reaction is now on, `removed` when it is now off — including when a `reacted` request found it already in that state and changed nothing."
                    },
                    "reacted": {
                      "type": "boolean",
                      "description": "True when the caller's reaction with this emoji is now ON; false when it is off."
                    },
                    "counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Reaction count per emoji on this item, after the call.",
                      "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`, \"Emoji not allowed.\"). Nothing changed.",
            "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 post 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, stage, decided, voting_closed }] }` — and **creates\nnothing**. Show them (\"add your vote, or post anyway\" — a vote only on a\nmatch that is not `decided`, not `voting_closed` and not already\n`has_voted`), 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 — offer the vote only on a match whose `decided`, `voting_closed` and `has_voted` are all false.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "vote_count",
                          "has_voted",
                          "voting_closed"
                        ],
                        "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
                          },
                          "stage": {
                            "type": "string",
                            "nullable": true,
                            "description": "The label of the lifecycle stage the match is in.",
                            "example": "Planned"
                          },
                          "decided": {
                            "type": "boolean",
                            "description": "True when the match is already in an Implemented- or Declined-category stage, so a client should not offer a vote on it.",
                            "example": false
                          },
                          "voting_closed": {
                            "type": "boolean",
                            "description": "True once the match's voting close date has passed — but only on a workspace where `close_date_enabled` is true. `POST /ideas/{idea_id}/vote` answers `422 voting_closed` on such a match, so a client should not offer a vote on it.",
                            "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 starts with;\n  changing it never moves an existing campaign. Only someone allowed to\n  choose a campaign's review panel (`can_assign_review_panel`, Ideas admins\n  by default) may pick another one on a 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 post 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 Value · 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 may carry file attachments (comments never do). 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. Every row also carries `voting_closed`\n— true once that date has passed in the workspace's time zone, always false\nwhile the feature is off — the same verdict `POST /ideas/{idea_id}/vote`\ngives.\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",
                          "voting_closed"
                        ],
                        "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
                          },
                          "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. The date ends at midnight in the WORKSPACE's time zone, so draw the padlock from this flag rather than by comparing `voting_closes_on` with the device's day.",
                            "example": false
                          },
                          "can_vote": {
                            "type": "boolean",
                            "description": "Whether this request may press the upvote at all: false once `voting_closed`, and for an API token without write:ideas, which `POST` / `DELETE /ideas/{idea_id}/vote` ask (a signed-in session always holds it). Taking a vote back also needs `vote_change_allowed`.",
                            "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`voting_closed` is on every row (always false while the feature is off).\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",
                          "voting_closed"
                        ],
                        "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
                          },
                          "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. The date ends at midnight in the WORKSPACE's time zone, so draw the padlock from this flag rather than by comparing `voting_closes_on` with the device's day.",
                            "example": false
                          },
                          "can_vote": {
                            "type": "boolean",
                            "description": "Whether this request may press the upvote at all: false once `voting_closed`, and for an API token without write:ideas, which `POST` / `DELETE /ideas/{idea_id}/vote` ask (a signed-in session always holds it). Taking a vote back also needs `vote_change_allowed`.",
                            "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 (by votes\nwhen RICE scoring is turned off). 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`.\nWith campaigns switched off in Ideas Settings, `campaigns` is empty and\nonly `none` is honoured — a campaign id falls back to `all`.\n\n**Awaiting-score sub-filter** (`needs`): when truthy, only ideas still\nwaiting for their first RICE score are returned — unscored and not yet in\nan Implemented or Declined stage. Ignored when RICE scoring is turned off\nfor the workspace.\n\n**`needs_scoring_count`** badges the \"awaiting score\" chip: the number of\nideas waiting for a first score 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. With RICE\nscoring turned off it is `up_votes_count` DESC, then `created_at` DESC,\nthen `id` DESC — the web queue's own fallback — so a score stored while\nscoring was on never decides the order.\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",
                          "voting_closed",
                          "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
                          },
                          "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. The date ends at midnight in the WORKSPACE's time zone, so draw the padlock from this flag rather than by comparing `voting_closes_on` with the device's day.",
                            "example": false
                          },
                          "can_vote": {
                            "type": "boolean",
                            "description": "Whether this request may press the upvote at all: false once `voting_closed`, and for an API token without write:ideas, which `POST` / `DELETE /ideas/{idea_id}/vote` ask (a signed-in session always holds it). Taking a vote back also needs `vote_change_allowed`.",
                            "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"
                          },
                          "scored_by_author": {
                            "type": "boolean",
                            "description": "True while the idea's only RICE score is the one its AUTHOR set — not the panel's review, so the idea is still waiting for one (the web row says \"Scored by its author\"). A reviewer confirms it by saving the same values (`PATCH /ideas/{idea_id}/score` answers `confirmed: true`). False once a colleague has scored or decided it, and while RICE scoring is off.",
                            "example": false
                          },
                          "stage_set_by_author": {
                            "type": "boolean",
                            "description": "True while the idea's decided stage is one its AUTHOR moved it to (the web row says \"Moved by its author\"). A reviewer confirms it by \"moving\" it to that same stage (`PATCH /ideas/{idea_id}/stage` answers `confirmed: true`). False once a colleague has acted.",
                            "example": false
                          }
                        }
                      }
                    },
                    "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"
                          },
                          "ended_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true,
                            "description": "ISO-8601 date the campaign actually ended, present only once its status is closed (null otherwise). A campaign closed early keeps a future end_date, so show this as its end.",
                            "example": "2026-08-14"
                          },
                          "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"
                        },
                        "ended_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601 date the campaign actually ended, present only once its status is closed (null otherwise). A campaign closed early keeps a future end_date, so show this as its end.",
                          "example": "2026-08-14"
                        },
                        "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"
                        },
                        "ended_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601 date the campaign actually ended, present only once its status is closed (null otherwise). A campaign closed early keeps a future end_date, so show this as its end.",
                          "example": "2026-08-14"
                        },
                        "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",
                              "voting_closed"
                            ],
                            "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
                              },
                              "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. The date ends at midnight in the WORKSPACE's time zone, so draw the padlock from this flag rather than by comparing `voting_closes_on` with the device's day.",
                                "example": false
                              },
                              "can_vote": {
                                "type": "boolean",
                                "description": "Whether this request may press the upvote at all: false once `voting_closed`, and for an API token without write:ideas, which `POST` / `DELETE /ideas/{idea_id}/vote` ask (a signed-in session always holds it). Taking a vote back also needs `vote_change_allowed`.",
                                "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**A close date before today closes the campaign.** On a campaign that is not\nalready closed (open or scheduled), a `close_date` (or `end_date` /\n`close_on`) earlier than today ends its submissions — the same act as\n`PATCH /ideas/campaigns/{id}/close` — so it asks the `destructive:ideas`\nscope **on top of** `write:ideas` (owner decision 2026-10-07, #7186). A token\nwithout it gets `403 insufficient_permissions` (\"Required scope:\ndestructive:ideas\") and nothing is written. Today's date, a future date, an\nempty one, and any date on a campaign already closed stay on `write:ideas`.\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"
                        },
                        "ended_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "ISO-8601 date the campaign actually ended, present only once its status is closed (null otherwise). A campaign closed early keeps a future end_date, so show this as its end.",
                          "example": "2026-08-14"
                        },
                        "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`). Or the token lacks `write:ideas` — or, for a close date before today on a campaign that is not closed, `destructive:ideas` on top (`insufficient_permissions`, #7186); nothing is written."
          },
          "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\nDelete-level (owner decision 2026-10-06, #6720 / #6721): requires the\n`destructive:ideas` scope.\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\nDelete-level (owner decision 2026-10-06, #6720 / #6721 — a close that\n`/reopen` takes back still asks the delete scope, #6768): requires the\n`destructive:ideas` scope. Reopening asks `write:ideas`.\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\n**Scope.** A move asks `write:ideas`. A move INTO a stage whose `category`\nis `declined` (the default pipeline's \"Deferred\") declines the idea, a\ndelete-level act (owner decision 2026-10-06, #6720 / #6721): it requires\nthe `destructive:ideas` scope on top of `write:ideas`, as the Ideas\nagent's `move_stage` does for the same move. A missing or unknown\n`stage_id` asks `write:ideas` and is answered `422` as below.\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
                    },
                    "confirmed": {
                      "type": "boolean",
                      "description": "`true` when a reviewer picked the decided stage the idea's AUTHOR had moved it to — the panel's review is now recorded (audit entry written, the panel's asks closed) though `changed` is false and `message` reads `Confirmed “<stage>”.` A retry of that is the plain no-op (`confirmed: false`).",
                      "example": false
                    },
                    "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 for this idea (false once its voting has closed).\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**Refused once voting is locked for the idea** — the workspace has\nvoting close dates on and the idea's `voting_closes_on` has passed: this\nendpoint returns `422 voting_closed` and the vote is kept, exactly as\ncasting one is refused. A vote taken off a closed idea could not be put\nback. Checked first, so it is the answer even when the next gate would\nalso refuse.\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. Both\ngates are checked BEFORE any delete, so they apply even when the caller\nhas no vote to remove. Otherwise removal is **idempotent** — removing\nwith 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`)."
          },
          "422": {
            "description": "Voting has closed for this idea (error code `voting_closed`)."
          }
        }
      }
    },
    "/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
                    },
                    "confirmed": {
                      "type": "boolean",
                      "description": "`true` when these values confirmed the score the idea's AUTHOR set — a reviewer's first save of the same values records the panel's review (one audit entry) and `message` reads `Score confirmed.`",
                      "example": false
                    },
                    "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\nDelete-level (owner decision 2026-10-06, #6720 / #6721): requires the\n`destructive:ideas` scope.\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* `rice_inputs` — the four values behind the score, for the scorer to\n  pre-fill; present only while scoring is on AND the caller is a reviewer.\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
                        },
                        "can_vote": {
                          "type": "boolean",
                          "description": "Whether this request may press the upvote at all: false once `voting_closed`, and for an API token without write:ideas, which `POST` / `DELETE /ideas/{idea_id}/vote` ask (a signed-in session always holds it). Taking a vote back also needs `vote_change_allowed`.",
                          "example": true
                        },
                        "vote_change_allowed": {
                          "type": "boolean",
                          "description": "Whether the caller may remove their upvote from THIS idea now — the workspace allows removing an upvote AND the idea's voting is not closed (drives the Remove affordance). False on an idea whose `voting_closed` is true: `DELETE /ideas/{idea_id}/vote` answers `422 voting_closed` there.",
                          "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"
                            }
                          }
                        },
                        "rice_inputs": {
                          "type": "object",
                          "description": "The four stored values behind `rice_score`, so the reviewer's scorer pre-fills from the columns rather than rebuilding them from the audit trail (an idea scored by sample data or a CSV import has no score entry, and the trail is capped at 50). Each is null when never set. Present only when RICE scoring is on AND `is_reviewer` is true — non-reviewers see only the total, like the web. `confidence` is the stored factor (1.0 / 0.8 / 0.5 / 0.3); null scores as 1.0.",
                          "properties": {
                            "reach": {
                              "type": "integer",
                              "nullable": true,
                              "description": "People per quarter.",
                              "example": 2600
                            },
                            "impact": {
                              "type": "number",
                              "nullable": true,
                              "description": "Per person: 3, 2, 1, 0.5 or 0.25.",
                              "example": 3.0
                            },
                            "confidence": {
                              "type": "number",
                              "nullable": true,
                              "description": "Stored confidence factor.",
                              "example": 0.5
                            },
                            "effort": {
                              "type": "number",
                              "nullable": true,
                              "description": "Person-months.",
                              "example": 0.5
                            }
                          }
                        },
                        "scored_by_author": {
                          "type": "boolean",
                          "description": "Present only when RICE scoring is on AND `is_reviewer` is true. True when the idea's score is the one its AUTHOR set — not the panel's review (the web says \"Scored by its author\"). A reviewer who is not the author (or the author, once nobody else is left on the panel) confirms it by saving the same values.",
                          "example": false
                        },
                        "stage_set_by_author": {
                          "type": "boolean",
                          "description": "Present only when `is_reviewer` is true. True when the idea sits on a decided stage its AUTHOR moved it to; a reviewer who is not the author (or the author alone on the panel) confirms it by \"moving\" it to that same stage (`PATCH /ideas/{idea_id}/stage`).",
                          "example": false
                        },
                        "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). The person's answer: it gates `audit_entries` below. Draw the stage mover and the RICE scorer from `can_move_stage` / `can_score`, which also ask the request's token for write:ideas.",
                          "example": true
                        },
                        "can_move_stage": {
                          "type": "boolean",
                          "description": "`is_reviewer` AND a credential holding write:ideas — the scope `PATCH …/stage` asks. A move into a declined stage asks destructive:ideas on top (per target stage, not in this flag).",
                          "example": true
                        },
                        "can_score": {
                          "type": "boolean",
                          "description": "`is_reviewer` AND write:ideas AND scoring on for the tenant — what `PATCH …/score` asks.",
                          "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?, category_from?, category_to?}` (stage ids); `score` → `{score_from, score_to, changes: [{field, from, to}]}`; `edit` → `{edits: [...]}`; `create` → `{}`. A `stage` entry with `category_from` / `category_to` (`entry`, `active`, `implemented` or `declined`) records an Ideas admin changing the CATEGORY of the stage the idea stands on, which decides or reopens every idea on it at once: the idea did not move, so `from` == `to`, and it is not a reviewer confirming the stage. A `stage` entry with `from` == `to` and no category keys is a reviewer confirming the decided stage the idea's author chose.",
                                "additionalProperties": true,
                                "example": {
                                  "from": 1,
                                  "to": 2,
                                  "note": "Looks good"
                                }
                              },
                              "by_author": {
                                "type": "boolean",
                                "description": "True on a `score` or `stage` entry made by the idea's own AUTHOR — a score or move that is not the review panel's. Never true on a lifecycle-stage reset's park entry nor on an Ideas admin re-categorising the stage (`category_to`), and always false on `create` / `edit` entries.",
                                "example": false
                              },
                              "from_stage": {
                                "type": "object",
                                "nullable": true,
                                "description": "`stage` entries ONLY (absent on other types): the payload's `from` id resolved for rendering. Null only when the entry has no `from` (an entry INTO the pipeline). A stage deleted since resolves to a stand-in carrying the stored `id`, `name` \"a stage since deleted\" (translated like the web) and a grey `color`; a renamed stage resolves to its current name.",
                                "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, including the \"a stage since deleted\" stand-in for a stage deleted since.",
                                "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**Adding attachments is gated.** `files[]` / `file_signed_ids[]` are\naccepted only while **\"Allow file attachments\"** is ON; when OFF they are\ndropped and the edit still applies. Sending new files never replaces an\nexisting one. **Removing is explicit:** `remove_file_ids[]` takes attachment\nids (each `attachments[].id` on the idea) off the idea — the web edit form's\nper-file \"Remove\" — whatever the toggle says; an id that is not one of this\nidea's attachments is ignored. Attach and remove run **inside the same\ntransaction as the save**, so a failure rolls the edit back rather than\ncommitting a half-applied change, and every attach or remove (a swap included) is recorded in\n`edits` as `Attachments`. Anything dropped is reported per-file in\n`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."
                  },
                  "remove_file_ids": {
                    "type": "array",
                    "description": "Attachment ids (`attachments[].id`) to remove from this idea. Ids that are not this idea's attachments are ignored.",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            },
            "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"
                    }
                  },
                  "remove_file_ids[]": {
                    "type": "array",
                    "description": "Attachment ids (`attachments[].id` from GET /api/v1/ideas/{id}) to remove from this idea. Ids that are not this idea's attachments are ignored. Honoured whatever the attachments toggle says.",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "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\nDelete-level (owner decision 2026-10-06, #6720 / #6721): requires the\n`destructive:ideas` scope.\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/courses/{course_id}/refresh": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Optional recall (GET)",
        "description": "Learner-owned. GET is read-only and hides answer keys until submission. POST returns practice state. Does not create formal quiz attempts. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Optional recall (POST)",
        "description": "Learner-owned. GET is read-only and hides answer keys until submission. POST returns practice state. Does not create formal quiz attempts. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope. Stopping also requires destructive:training.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "subscribe",
                      "start",
                      "answer",
                      "pause",
                      "resume",
                      "snooze",
                      "stop"
                    ]
                  },
                  "session_id": {
                    "type": "integer"
                  },
                  "answers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/courses/{course_id}/refresh_sets": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Review refresh question copies (GET)",
        "description": "Training admin only. GET accepts lesson_id to return the exact review source. POST explicitly approves selected keys and requires the matching digest. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "lesson_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated Training collection with the standard items/meta envelope, compatibility alias, and unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta",
                    "total_count",
                    "sets",
                    "total",
                    "can_approve",
                    "unread_notification_count"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "sets": {
                      "type": "array",
                      "description": "Compatibility alias of items.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Compatibility alias of total_count."
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page",
                        "has_next_page",
                        "has_prev_page"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "total_pages": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "current_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "has_next_page": {
                          "type": "boolean"
                        },
                        "has_prev_page": {
                          "type": "boolean"
                        }
                      }
                    },
                    "can_approve": {
                      "type": "boolean"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "source": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true
                    }
                  },
                  "additionalProperties": true
                },
                "example": {
                  "items": [],
                  "sets": [],
                  "total_count": 0,
                  "total": 0,
                  "meta": {
                    "total_count": 0,
                    "total_pages": 0,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "can_approve": false,
                  "unread_notification_count": null,
                  "source": null
                }
              }
            },
            "headers": {
              "X-Total-Count": {
                "description": "Total accessible rows",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Number of pages",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "Current page",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "Page size (20)",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Review refresh question copies (POST)",
        "description": "Training admin only. GET accepts lesson_id to return the exact review source. POST explicitly approves selected keys and requires the matching digest. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lesson_id": {
                    "type": "integer"
                  },
                  "digest": {
                    "type": "string"
                  },
                  "keys": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "reviewed": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/courses/{course_id}/refresh_sets/{id}": {
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Withdraw refresh approval (PATCH)",
        "description": "Training admin only. Stops future sessions; existing results remain. Requires destructive:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      }
    },
    "/training/courses/{course_id}/language_variants": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Link language editions (GET)",
        "description": "Training admin only. Creates an unapproved link; grants no credit. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated Training collection with the standard items/meta envelope, compatibility alias, and unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta",
                    "total_count",
                    "variants",
                    "total",
                    "can_link",
                    "unread_notification_count"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "variants": {
                      "type": "array",
                      "description": "Compatibility alias of items.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Compatibility alias of total_count."
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page",
                        "has_next_page",
                        "has_prev_page"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "total_pages": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "current_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "has_next_page": {
                          "type": "boolean"
                        },
                        "has_prev_page": {
                          "type": "boolean"
                        }
                      }
                    },
                    "can_link": {
                      "type": "boolean"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  },
                  "additionalProperties": true
                },
                "example": {
                  "items": [],
                  "variants": [],
                  "total_count": 0,
                  "total": 0,
                  "meta": {
                    "total_count": 0,
                    "total_pages": 0,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "can_link": false,
                  "unread_notification_count": null
                }
              }
            },
            "headers": {
              "X-Total-Count": {
                "description": "Total accessible rows",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Number of pages",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "Current page",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "Page size (20)",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Link language editions (POST)",
        "description": "Training admin only. Creates an unapproved link; grants no credit. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variant_course_id": {
                    "type": "integer"
                  },
                  "locale": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/courses/{course_id}/language_variants/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Review exact language equivalence (GET)",
        "description": "Training admin only. Independent reviewer must inspect all coverage and match both digests. Withdraw ends future credit and releases the pending choices it held (each learner told in the app); a link nobody approved can be removed. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Review exact language equivalence (PATCH)",
        "description": "Training admin only. Independent reviewer must inspect all coverage and match both digests. Withdraw ends future credit and releases the pending choices it held (each learner told in the app); a link nobody approved can be removed. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope. Withdrawal also requires destructive:training.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "withdraw",
                      "remove",
                      "release",
                      "release_pending"
                    ],
                    "description": "approve (write:training) — an independent admin approves the exact editions. withdraw (destructive:training) — ends future credit and releases every pending choice in the background; the response's `releasing` is how many. remove (destructive:training) — deletes a link nobody approved (`can_remove`). release (destructive:training, with `enrollment_id` or `choice_id`) — ends one learner's unfulfilled choice. release_pending (destructive:training) — releases what a withdrawn approval still holds (`pending_choices`, `can_release_pending`)."
                  },
                  "choice_id": {
                    "type": "integer",
                    "description": "With decision=release: the language choice to end."
                  },
                  "enrollment_id": {
                    "type": "integer",
                    "description": "With decision=release, instead of choice_id: the learner's assignment, or the language course enrollment chosen for it, whose choice of this edition pair to end."
                  },
                  "coverage": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "source_digest": {
                    "type": "string"
                  },
                  "variant_digest": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/enrollments/{enrollment_id}/language_choice": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Own language choice (GET)",
        "description": "Learner-owned current assignment. Choose or remove only before work. GET returns approved options, chosen actual enrollment, fulfilled_at and can_choose. DELETE withdraws the choice with destructive:training; an unstarted, unassigned language-course enrollment the choice itself created is cancelled, and learning history is kept. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Own language choice (POST)",
        "description": "Learner-owned current assignment. Choose or remove only before work. GET returns approved options, chosen actual enrollment, fulfilled_at and can_choose. DELETE withdraws the choice with destructive:training; an unstarted, unassigned language-course enrollment the choice itself created is cancelled, and learning history is kept. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variant_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Own language choice (DELETE)",
        "description": "Learner-owned current assignment. Choose or remove only before work. GET returns approved options, chosen actual enrollment, fulfilled_at and can_choose. DELETE withdraws the choice with destructive:training; an unstarted, unassigned language-course enrollment the choice itself created is cancelled, and learning history is kept. Requires destructive:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variant_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/contributions": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Private learning contributions (GET)",
        "description": "GET supports review=1 for the authorized pending queue and review=history for verified, rejected or withdrawn entries in the same reviewer scope and course_id for eligible source choices. Otherwise lists own entries. Twenty per page. Submission requires own course enrollment; public consent defaults false. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "review",
            "in": "query",
            "description": "Omit to list own contributions.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "history"
              ]
            }
          },
          {
            "name": "course_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated Training collection with the standard items/meta envelope, compatibility alias, and unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta",
                    "total_count",
                    "contributions",
                    "total",
                    "can_submit",
                    "unread_notification_count"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "contributions": {
                      "type": "array",
                      "description": "Compatibility alias of items.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Compatibility alias of total_count."
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "total_count",
                        "total_pages",
                        "current_page",
                        "per_page",
                        "has_next_page",
                        "has_prev_page"
                      ],
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "total_pages": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "current_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "has_next_page": {
                          "type": "boolean"
                        },
                        "has_prev_page": {
                          "type": "boolean"
                        }
                      }
                    },
                    "can_submit": {
                      "type": "boolean"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "page": {
                      "type": "integer",
                      "description": "Compatibility alias of meta.current_page."
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  },
                  "additionalProperties": true
                },
                "example": {
                  "items": [],
                  "contributions": [],
                  "total_count": 0,
                  "total": 0,
                  "meta": {
                    "total_count": 0,
                    "total_pages": 0,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "can_submit": false,
                  "unread_notification_count": null,
                  "page": 1,
                  "sources": []
                }
              }
            },
            "headers": {
              "X-Total-Count": {
                "description": "Total accessible rows",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Number of pages",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "Current page",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "Page size (20)",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Private learning contributions (POST)",
        "description": "GET supports review=1 for the authorized pending queue and course_id for eligible source choices. Otherwise lists own entries. Twenty per page. Submission requires own course enrollment; public consent defaults false. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "course_id": {
                    "type": "integer"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "application",
                      "helpful_answer",
                      "published_correction"
                    ]
                  },
                  "source_id": {
                    "type": "integer"
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "public_consent": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/contributions/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Review a learning contribution (GET)",
        "description": "An independent manager/course reviewer verifies evidence. Application requires observed=true. Recognition additionally requires write:recognition and Recognitions permission; explicit reviewed message and program rules apply. Zero value, one award per contribution. Withdraw/revoke also revokes its award. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Review a learning contribution (PATCH)",
        "description": "An independent manager/course reviewer verifies evidence. Application requires observed=true. Recognition additionally requires write:recognition and Recognitions permission; explicit reviewed message and program rules apply. Zero value, one award per contribution. Withdraw/revoke also revokes its award. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope. Withdraw, reject and revoke also require destructive:training; withdrawing or revoking an awarded contribution additionally requires destructive:recognition. Own-recognition tokens retain only direct-report recipient access.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "resubmit",
                      "withdraw",
                      "verify",
                      "needs_detail",
                      "reject",
                      "revoke",
                      "recognize"
                    ]
                  },
                  "body": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  },
                  "observed": {
                    "type": "boolean"
                  },
                  "message": {
                    "type": "string"
                  },
                  "category_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/courses/{course_id}/versions/{version_id}/briefing": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Review edition briefing (GET)",
        "description": "Course content manager only. GET returns source/target digests and the prior reviewed receipt. PATCH requires human review and exact matching editions. Learner course detail returns version_brief including the explanation only while fresh. Requires read:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        }
      },
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Review edition briefing (PATCH)",
        "description": "Course content manager only. GET returns source/target digests and the prior reviewed receipt. PATCH requires human review and exact matching editions. Learner course detail returns version_brief including the explanation only while fresh. Requires write:training plus Training app access and the per-record role. Errors use the standard error envelope.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Training payload plus unread_notification_count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "403": {
            "description": "Missing scope, app access or role"
          },
          "404": {
            "description": "Record outside the permitted scope"
          },
          "422": {
            "description": "Validation or stale review refused; nothing changed"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "summary": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 5000
                  },
                  "reviewed": {
                    "type": "boolean"
                  },
                  "source_digest": {
                    "type": "string"
                  },
                  "target_digest": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/training/return_reminders": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Read your optional learning reminder schedule",
        "description": "Requires read:training. Self only. No schedule is enabled by default. Return and recall practice share a one-per-local-day and two-per-rolling-seven-days cap. can_change reflects write:training.",
        "parameters": [
          {
            "name": "enrollment_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Personal schedule, timezone, daily_limit, weekly_limit, optional snoozed_until, can_snooze and scope-aware can_change, with unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingReturnReminderResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope or Training access."
          }
        }
      },
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Set or pause your optional learning reminder schedule",
        "description": "Requires write:training. Times use the learner's effective timezone. Omit or clear delivery_hour to turn optional reminders off. Mandatory deadlines and reminders are unchanged. In-app and push channels obey personal notification preferences and quiet hours.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "delivery_hour": {
                    "type": "integer",
                    "nullable": true,
                    "enum": [
                      9,
                      12,
                      17,
                      null
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved reminders and can_change, with unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingReturnReminderResponse"
                }
              }
            }
          },
          "422": {
            "description": "Standard structured validation error."
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Remind me later or undo a course snooze",
        "description": "Requires write:training. Only your current, available, active course attempt is eligible. A snooze sets your preferred hour and suppresses this course until the chosen date; actual delivery remains subject to shared caps, deadlines, access, activity and notification preferences. Undo clears the snooze, not the preferred schedule. No progress or deadline change.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enrollment_id",
                  "days"
                ],
                "properties": {
                  "enrollment_id": {
                    "type": "integer"
                  },
                  "days": {
                    "type": "string",
                    "enum": [
                      "1",
                      "3",
                      "7",
                      "undo"
                    ]
                  },
                  "delivery_hour": {
                    "type": "integer",
                    "enum": [
                      9,
                      12,
                      17
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved reminders with snoozed_until and can_change, with unread_notification_count.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingReturnReminderResponse"
                }
              }
            }
          },
          "403": {
            "description": "Missing write scope or Training access."
          },
          "422": {
            "description": "Invalid action or unavailable enrollment; standard structured error."
          }
        }
      }
    },
    "/training/checklists": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Checklists to sign",
        "description": "The learners whose sign-off checklist items the caller signs (a course's\nEvaluators and Instructors, a Training admin, a class's instructor, or a\nmanager where the checklist lets managers sign), soonest due first — the\nweb and phone \"Checklists to sign\" list. `status=waiting` (default) is\nthe learners with something still to sign; `all` adds the finished ones.\n`courses` lists what `course_id` can narrow to. Someone who signs nothing\nis refused (403) with the sentence to show.\n\n**Token scope:** `read:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "waiting",
                "all"
              ]
            }
          },
          {
            "name": "course_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A learner's name."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of learners.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "active_status": {
                      "type": "string",
                      "enum": [
                        "waiting",
                        "all"
                      ]
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "enrollment_id": {
                            "type": "integer"
                          },
                          "lesson": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "title": {
                                "type": "string"
                              }
                            }
                          },
                          "course": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "title": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "learner": {
                            "$ref": "#/components/schemas/TrainingChecklistPerson"
                          },
                          "due_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "items_total": {
                            "type": "integer"
                          },
                          "items_passing": {
                            "type": "integer",
                            "description": "Items holding a current Met or N/A."
                          }
                        }
                      }
                    },
                    "courses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — the caller signs no checklist; or Training is not open to them, or the token lacks `read:training`."
          }
        }
      }
    },
    "/training/checklists/{lesson_id}/learners/{enrollment_id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "One learner's sign-off checklist",
        "description": "The checklist item by item, with each item's current sign-off, the\nresults it takes (N/A only where the item allows it), whether a comment\nis required, and whether the caller may sign or withdraw. `sign_refusal`\nsays why nothing can be signed now (a completed or archived course, a\nlearner who has left, or the caller not a signer). Readable by whoever\nmay sign it or reopen it; the learner reads their own in the lesson.\n\n**Token scope:** `read:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The checklist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingChecklistAttempt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a signer or reopener of this learner's checklist (the learner themself included); the message says why."
          },
          "404": {
            "description": "`not_found` — no such checklist lesson, or no such learner on its course."
          }
        }
      }
    },
    "/training/checklists/{lesson_id}/learners/{enrollment_id}/items/{item_key}/sign": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Sign one checklist item",
        "description": "Records a sign-off for one item, as the web sign-off page does: the\nsame rule for who may sign (never the learner), the same checks on the\nresult, the validation method and the comment. Answers the sign-off and\nthe checklist as it now stands.\n\n**Token scope:** `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "item_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,16}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "result",
                  "validation_method"
                ],
                "properties": {
                  "result": {
                    "type": "string",
                    "enum": [
                      "met",
                      "unmet",
                      "not_applicable"
                    ],
                    "description": "One of the item's own `results`."
                  },
                  "validation_method": {
                    "type": "string",
                    "description": "One of the checklist's `validation_methods`."
                  },
                  "comment": {
                    "type": "string",
                    "description": "Required where the item's `comment_required` is true."
                  },
                  "signed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the item was signed on the device, for a sign-off made offline and sent later. Absent means now. Recorded as the sign-off's `signed_at`; who may sign is still checked when it arrives. Refused (422) when more than 5 minutes ahead of the server's clock or more than 7 days old; a time up to 5 minutes ahead is recorded as now. Refused (409) when the item was signed, withdrawn or reopened after it.",
                    "example": "2026-11-02T09:41:00Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingChecklistAttempt"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "signoff": {
                          "$ref": "#/components/schemas/TrainingChecklistSignoff"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a signer; the record can no longer take a sign-off (a completed course, an archived course, a learner who has left); or the token lacks `write:training`. The message says which."
          },
          "404": {
            "description": "`not_found` — no such checklist, learner or item."
          },
          "409": {
            "description": "`conflict` — someone else signed this item at the same moment, or (with `signed_at`) the item was signed, withdrawn or reopened after the device's time; reload the checklist."
          },
          "422": {
            "description": "`invalid` — a result the item does not take, an unknown validation method, a missing required comment, or a `signed_at` that is unreadable, in the future, or more than 7 days old."
          }
        }
      }
    },
    "/training/checklists/{lesson_id}/learners/{enrollment_id}/signoffs/{signoff_id}/withdraw": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Withdraw a sign-off",
        "description": "Takes a current sign-off back out of the learner's record, with a\nreason; it stays in the item's history and the learner is told. The\nperson who gave it (while they may still sign), or anyone who may reopen\nthe checklist. Reopening a whole checklist stays on the desktop page.\n\n**Token scope:** `destructive:training` (a final act).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "signoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Withdrawn; the checklist as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingChecklistAttempt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not allowed to withdraw this sign-off; the record is closed (a completed course, an archived course, a learner who has left); or the token lacks `destructive:training`."
          },
          "404": {
            "description": "`not_found` — no such checklist lesson, or no such learner on its course."
          },
          "409": {
            "description": "`conflict` — the sign-off is not a current one on this checklist (already changed or withdrawn); reload the checklist."
          },
          "422": {
            "description": "`invalid` — no reason, or one too long (the message gives the limit)."
          }
        }
      }
    },
    "/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` — LEGACY, may overlap: badges for the All / In Progress /\n  Assigned / Completed tabs plus the Overdue banner, unchanged for shipped\n  apps. in-progress/assigned/overdue are measured over the active set (an\n  assigned, in-progress course counts in both In Progress and Assigned; an\n  overdue one in In Progress and Overdue), completed over the completed\n  set. `all` is every enrollment except cancelled.\n* `bucket_counts` — the PARTITION the web My Learning chips use\n  (Training::LearningStatus): not_started + in_progress + overdue is every\n  open enrollment, each counted once (overdue first). Pair it with the\n  `bucket` filter on `/training/my_training/enrollments`.\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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "minutes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                3,
                5,
                10
              ]
            },
            "description": "Optional time preference; never promises a formal assessment will fit."
          }
        ],
        "responses": {
          "200": {
            "description": "My Training home retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "my_training"
                  ],
                  "properties": {
                    "my_training": {
                      "type": "object",
                      "required": [
                        "counts",
                        "bucket_counts",
                        "overdue",
                        "continue_learning"
                      ],
                      "properties": {
                        "next_step": {
                          "type": "object",
                          "nullable": true,
                          "description": "One eligible next activity, or null. Read-only selection does not start an enrollment. Native estimated/measured duration is advisory; assessments and opaque packages never claim to fit a short budget.",
                          "properties": {
                            "course_id": {
                              "type": "integer"
                            },
                            "lesson_id": {
                              "type": "integer"
                            },
                            "enrollment_id": {
                              "type": "integer"
                            },
                            "course_version_id": {
                              "type": "integer"
                            },
                            "course_title": {
                              "type": "string"
                            },
                            "lesson_title": {
                              "type": "string"
                            },
                            "url": {
                              "type": "string"
                            },
                            "mobile_url": {
                              "type": "string"
                            },
                            "estimated_seconds": {
                              "type": "integer",
                              "nullable": true
                            },
                            "reason": {
                              "type": "string"
                            },
                            "fits_budget": {
                              "type": "boolean"
                            },
                            "stop_after_lesson": {
                              "type": "boolean"
                            }
                          }
                        },
                        "counts": {
                          "type": "object",
                          "description": "LEGACY, may overlap. Badges for the All / In Progress / Assigned / Completed tabs + Overdue banner, unchanged for shipped apps: all but `all` OVERLAP (an assigned, in-progress course counts in both In Progress and Assigned; an overdue one in In Progress and Overdue). `all` is the union of the others — every enrollment except cancelled — and equals `meta.total_count` on `/training/my_training/enrollments?status=all`. New clients read `bucket_counts`.",
                          "required": [
                            "all",
                            "in_progress",
                            "assigned",
                            "completed",
                            "overdue"
                          ],
                          "properties": {
                            "all": {
                              "type": "integer",
                              "example": 8
                            },
                            "in_progress": {
                              "type": "integer",
                              "description": "Legacy",
                              "may overlap overdue.": null,
                              "example": 6
                            },
                            "assigned": {
                              "type": "integer",
                              "description": "Legacy — provenance",
                              "not a status; may overlap.": null,
                              "example": 3
                            },
                            "completed": {
                              "type": "integer",
                              "example": 2
                            },
                            "overdue": {
                              "type": "integer",
                              "example": 2
                            }
                          }
                        },
                        "bucket_counts": {
                          "type": "object",
                          "description": "The PARTITION of the learner's open enrollments the web My Learning chips use (Training::LearningStatus): every open course or path is in exactly one bucket, overdue first, so not_started + in_progress + overdue is every open enrollment. Each equals `meta.total_count` on `/training/my_training/enrollments?bucket=<key>`.",
                          "required": [
                            "not_started",
                            "in_progress",
                            "overdue"
                          ],
                          "properties": {
                            "not_started": {
                              "type": "integer",
                              "description": "Not begun and not overdue — self-enrolled included.",
                              "example": 2
                            },
                            "in_progress": {
                              "type": "integer",
                              "description": "Started and not overdue.",
                              "example": 3
                            },
                            "overdue": {
                              "type": "integer",
                              "description": "Past due",
                              "started or not.": null,
                              "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
                                  }
                                }
                              }
                            }
                          }
                        },
                        "external_training": {
                          "type": "object",
                          "description": "Courses still to do in the company's OTHER learning system (Training Connect: Workday, Cornerstone, a custom LMS), soonest deadline first. Each is taken in that system: open `start_url` in the browser. Empty (`total` 0) when the company connects no other system or nothing is open there.",
                          "required": [
                            "total",
                            "items"
                          ],
                          "properties": {
                            "total": {
                              "type": "integer",
                              "description": "Open courses there",
                              "counted before the list is capped at 20.": null,
                              "example": 3
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "title",
                                  "overdue",
                                  "days_overdue",
                                  "in_progress"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "example": 918
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Sea Survival"
                                  },
                                  "due_date": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true,
                                    "example": "2026-10-20"
                                  },
                                  "overdue": {
                                    "type": "boolean",
                                    "example": false
                                  },
                                  "days_overdue": {
                                    "type": "integer",
                                    "example": 0
                                  },
                                  "in_progress": {
                                    "type": "boolean",
                                    "description": "true once the other system reports it started — label the button Resume.",
                                    "example": false
                                  },
                                  "start_url": {
                                    "type": "string",
                                    "format": "uri",
                                    "nullable": true,
                                    "description": "The course's page in the other system; null when the sync gave no web address (show no button).",
                                    "example": "https://lms.example.com/courses/sea-survival"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "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. A course fulfilled by an approved language edition reports fulfilled; paths do not use that state.",
                                "enum": [
                                  "enrolled",
                                  "in_progress",
                                  "completed",
                                  "cancelled",
                                  "fulfilled"
                                ],
                                "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 — the badge word the web and phone cards print (In Progress / Not Started / Overdue / Completed / Registered / Cancelled). An open enrollment past its due date reads Overdue here, and a withdrawn attempt Cancelled, while status_key keeps its own enum (not_started or in_progress).",
                                "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",
                                "description": "Actual completion of this course or path. Language fulfillment does not copy a completion.",
                                "example": false
                              },
                              "satisfied": {
                                "type": "boolean",
                                "description": "Whether the assignment is complete or fulfilled through an approved language edition.",
                                "example": false
                              },
                              "fulfilled_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "When approved language credit satisfied this course assignment. Paths return null."
                              },
                              "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, or outside the counted set, are excluded, so this can never exceed the total.",
                                "example": 3
                              },
                              "total_lessons": {
                                "type": "integer",
                                "description": "Course cards only. The lessons `progress_percentage` is computed from: the REQUIRED, playable lessons of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — an optional lesson, or a partner or practice lesson that cannot be played right now, is left out, so the pair always agrees with the percentage. On a course with no required lesson, every lesson the learner can play (the course completes when they are done). A completed enrollment reports `completed_lessons_count` equal to `total_lessons`. The outline's own row count is `/training/courses/{id}/lessons`.",
                                "example": 5
                              },
                              "required_lessons_only": {
                                "type": "boolean",
                                "description": "Course cards only. True when `total_lessons` leaves some of the outline out (an optional or unplayable lesson), so a client can say \"3/5 required lessons\".",
                                "example": true
                              },
                              "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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 (or soonest due with `sort=due`),\nmatching the prototype's mixed list.\nServes the segmented filter: All / In Progress / Assigned / Completed /\nOverdue (`status`, legacy), and the web chips' partition (`bucket`). Every\n`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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "LEGACY, may overlap. 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; `in_progress` and `assigned` include overdue rows, and `assigned` is provenance. Every value takes the same `page`/`per_page` contract and returns the same `enrollments` + `counts` + `meta` shape. Ignored when `bucket` is given.",
            "schema": {
              "type": "string",
              "enum": [
                "in_progress",
                "assigned",
                "completed",
                "overdue",
                "all"
              ],
              "default": "in_progress",
              "example": "in_progress"
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "description": "One bucket of the web chips' PARTITION of the open list (Training::LearningStatus): `not_started` (self-enrolled included), `in_progress` or `overdue` — each open course or path is in exactly one, overdue first. When given, the list is that bucket and `status` is not applied; `active_bucket` echoes it and `bucket_counts` badges it. An unknown value is ignored (the `status` list is returned).",
            "schema": {
              "type": "string",
              "enum": [
                "not_started",
                "in_progress",
                "overdue"
              ],
              "example": "not_started"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "The order of the list — the web and phone My Learning's Sort by (Training::LearnerListOrder). `newest` (the default, and this list's only order before the parameter existed): newest assigned first, the enrollment's creation time, courses and paths interleaved. `due`: soonest due date first, so overdue rows lead; open rows with no due date follow every dated one, and completed rows come last, newest first within each. It orders the page only — `counts`, `bucket_counts` and `meta.total_count` are the same either way. An unknown value is `newest`; `active_sort` echoes the one applied.",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "due"
              ],
              "default": "newest",
              "example": "newest"
            }
          },
          {
            "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",
                    "active_bucket",
                    "active_sort",
                    "counts",
                    "bucket_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. A course fulfilled by an approved language edition reports fulfilled; paths do not use that state.",
                            "enum": [
                              "enrolled",
                              "in_progress",
                              "completed",
                              "cancelled",
                              "fulfilled"
                            ],
                            "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 — the badge word the web and phone cards print (In Progress / Not Started / Overdue / Completed / Registered / Cancelled). An open enrollment past its due date reads Overdue here, and a withdrawn attempt Cancelled, while status_key keeps its own enum (not_started or in_progress).",
                            "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",
                            "description": "Actual completion of this course or path. Language fulfillment does not copy a completion.",
                            "example": false
                          },
                          "satisfied": {
                            "type": "boolean",
                            "description": "Whether the assignment is complete or fulfilled through an approved language edition.",
                            "example": false
                          },
                          "fulfilled_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When approved language credit satisfied this course assignment. Paths return null."
                          },
                          "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, or outside the counted set, are excluded, so this can never exceed the total.",
                            "example": 3
                          },
                          "total_lessons": {
                            "type": "integer",
                            "description": "Course cards only. The lessons `progress_percentage` is computed from: the REQUIRED, playable lessons of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — an optional lesson, or a partner or practice lesson that cannot be played right now, is left out, so the pair always agrees with the percentage. On a course with no required lesson, every lesson the learner can play (the course completes when they are done). A completed enrollment reports `completed_lessons_count` equal to `total_lessons`. The outline's own row count is `/training/courses/{id}/lessons`.",
                            "example": 5
                          },
                          "required_lessons_only": {
                            "type": "boolean",
                            "description": "Course cards only. True when `total_lessons` leaves some of the outline out (an optional or unplayable lesson), so a client can say \"3/5 required lessons\".",
                            "example": true
                          },
                          "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",
                      "nullable": true,
                      "enum": [
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue",
                        "all"
                      ],
                      "description": "The `status` this page lists; null when `bucket` drove the list instead.",
                      "example": "in_progress"
                    },
                    "active_bucket": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "not_started",
                        "in_progress",
                        "overdue"
                      ],
                      "description": "The `bucket` this page lists; null when the `status` list was returned.",
                      "example": null
                    },
                    "active_sort": {
                      "type": "string",
                      "enum": [
                        "newest",
                        "due"
                      ],
                      "description": "The order this page is in (`sort`, defaulting to `newest`).",
                      "example": "newest"
                    },
                    "counts": {
                      "type": "object",
                      "description": "LEGACY, may overlap. Badges for the All / In Progress / Assigned / Completed tabs + Overdue banner, unchanged for shipped apps: all but `all` OVERLAP (an assigned, in-progress course counts in both In Progress and Assigned; an overdue one in In Progress and Overdue). `all` is the union of the others — every enrollment except cancelled — and equals `meta.total_count` on `/training/my_training/enrollments?status=all`. New clients read `bucket_counts`.",
                      "required": [
                        "all",
                        "in_progress",
                        "assigned",
                        "completed",
                        "overdue"
                      ],
                      "properties": {
                        "all": {
                          "type": "integer",
                          "example": 8
                        },
                        "in_progress": {
                          "type": "integer",
                          "description": "Legacy",
                          "may overlap overdue.": null,
                          "example": 6
                        },
                        "assigned": {
                          "type": "integer",
                          "description": "Legacy — provenance",
                          "not a status; may overlap.": null,
                          "example": 3
                        },
                        "completed": {
                          "type": "integer",
                          "example": 2
                        },
                        "overdue": {
                          "type": "integer",
                          "example": 2
                        }
                      }
                    },
                    "bucket_counts": {
                      "type": "object",
                      "description": "The PARTITION of the learner's open enrollments the web My Learning chips use (Training::LearningStatus): every open course or path is in exactly one bucket, overdue first, so not_started + in_progress + overdue is every open enrollment. Each equals `meta.total_count` on `/training/my_training/enrollments?bucket=<key>`.",
                      "required": [
                        "not_started",
                        "in_progress",
                        "overdue"
                      ],
                      "properties": {
                        "not_started": {
                          "type": "integer",
                          "description": "Not begun and not overdue — self-enrolled included.",
                          "example": 2
                        },
                        "in_progress": {
                          "type": "integer",
                          "description": "Started and not overdue.",
                          "example": 3
                        },
                        "overdue": {
                          "type": "integer",
                          "description": "Past due",
                          "started or not.": null,
                          "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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 created while the tenant setting is on allows self-enrollment\nunless an admin turns it off (one created while it is off starts with it\noff), a learning path denies it unless an admin turns it on — so the same\ntenant can legitimately 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\n**Required Scopes:** `write:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:training\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\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. A course fulfilled by an approved language edition reports fulfilled; paths do not use that state.",
                          "enum": [
                            "enrolled",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "fulfilled"
                          ],
                          "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 — the badge word the web and phone cards print (In Progress / Not Started / Overdue / Completed / Registered / Cancelled). An open enrollment past its due date reads Overdue here, and a withdrawn attempt Cancelled, while status_key keeps its own enum (not_started or in_progress).",
                          "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",
                          "description": "Actual completion of this course or path. Language fulfillment does not copy a completion.",
                          "example": false
                        },
                        "satisfied": {
                          "type": "boolean",
                          "description": "Whether the assignment is complete or fulfilled through an approved language edition.",
                          "example": false
                        },
                        "fulfilled_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When approved language credit satisfied this course assignment. Paths return null."
                        },
                        "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, or outside the counted set, are excluded, so this can never exceed the total.",
                          "example": 3
                        },
                        "total_lessons": {
                          "type": "integer",
                          "description": "Course cards only. The lessons `progress_percentage` is computed from: the REQUIRED, playable lessons of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — an optional lesson, or a partner or practice lesson that cannot be played right now, is left out, so the pair always agrees with the percentage. On a course with no required lesson, every lesson the learner can play (the course completes when they are done). A completed enrollment reports `completed_lessons_count` equal to `total_lessons`. The outline's own row count is `/training/courses/{id}/lessons`.",
                          "example": 5
                        },
                        "required_lessons_only": {
                          "type": "boolean",
                          "description": "Course cards only. True when `total_lessons` leaves some of the outline out (an optional or unplayable lesson), so a client can say \"3/5 required lessons\".",
                          "example": true
                        },
                        "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 created while the tenant setting is on allows self-enrollment\nunless an admin turns it off (one created while it is off starts with it\noff), a learning path denies it unless an admin turns it on — so the same\ntenant can legitimately 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\n**Required Scopes:** `write:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"). A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:training\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\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. A course fulfilled by an approved language edition reports fulfilled; paths do not use that state.",
                          "enum": [
                            "enrolled",
                            "in_progress",
                            "completed",
                            "cancelled",
                            "fulfilled"
                          ],
                          "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 — the badge word the web and phone cards print (In Progress / Not Started / Overdue / Completed / Registered / Cancelled). An open enrollment past its due date reads Overdue here, and a withdrawn attempt Cancelled, while status_key keeps its own enum (not_started or in_progress).",
                          "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",
                          "description": "Actual completion of this course or path. Language fulfillment does not copy a completion.",
                          "example": false
                        },
                        "satisfied": {
                          "type": "boolean",
                          "description": "Whether the assignment is complete or fulfilled through an approved language edition.",
                          "example": false
                        },
                        "fulfilled_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When approved language credit satisfied this course assignment. Paths return null."
                        },
                        "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, or outside the counted set, are excluded, so this can never exceed the total.",
                          "example": 3
                        },
                        "total_lessons": {
                          "type": "integer",
                          "description": "Course cards only. The lessons `progress_percentage` is computed from: the REQUIRED, playable lessons of the version the learner is PINNED to (falling back to the current published version when not yet pinned) — an optional lesson, or a partner or practice lesson that cannot be played right now, is left out, so the pair always agrees with the percentage. On a course with no required lesson, every lesson the learner can play (the course completes when they are done). A completed enrollment reports `completed_lessons_count` equal to `total_lessons`. The outline's own row count is `/training/courses/{id}/lessons`.",
                          "example": 5
                        },
                        "required_lessons_only": {
                          "type": "boolean",
                          "description": "Course cards only. True when `total_lessons` leaves some of the outline out (an optional or unplayable lesson), so a client can say \"3/5 required lessons\".",
                          "example": true
                        },
                        "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 `destructive:training` scope** — a delete-level act\n(#7183, #6721 \"classify by the act\"). 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 `destructive: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 `destructive:training` scope** — a delete-level act\n(#7183, #6721 \"classify by the act\"). 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 `destructive: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). Only what the signed-in learner can get into is\nlisted: an item with self-enrollment off — its own switch, or the\norganization's Allow Self-Enrollment setting in Training Settings — appears\nonly to learners enrolled in it, and a paid course stays listed so it can be\nbought. A course and the editions linked to it as its translations (Course ›\nLanguages) are one item: the edition in the language the learner reads\ncontent in, else the original — unless the learner's catalog lists neither,\nwhen the editions it does list stay. An edition the learner already holds is\nalways listed. `counts` and\n`/training/catalog/categories` follow the same rule. Reconciled to the mobile\nprototype'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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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, what the caller has already finished last) · 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"
                          },
                          "new": {
                            "type": "boolean",
                            "description": "Published in the last 30 days — the web catalog's New badge and the phone's New chip, from the same rule.",
                            "example": false
                          },
                          "enrollment_status": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "enrolled",
                              "in_progress",
                              "completed",
                              "cancelled",
                              "fulfilled"
                            ],
                            "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, a priced course on a tenant without commerce, or an API token without write:training (the scope every enrol-style POST asks). 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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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 3, 2026"
                          },
                          "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": "The chip's word — Active, Expiring Soon, Expired or No Expiry (one vocabulary with the web). The day count is `days_until_expiration`.",
                                "example": "No Expiry"
                              },
                              "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/..."
                          },
                          "can_share_outside": {
                            "type": "boolean",
                            "description": "Whether the learner may share this certificate outside the company (POST /moment_shares, subject_type certificate) — its card and its verification link. False while the company's Shared moments switch is off, or for a token without write:training.",
                            "example": true
                          }
                        }
                      }
                    },
                    "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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 3, 2026"
                        },
                        "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": "The chip's word — Active, Expiring Soon, Expired or No Expiry (one vocabulary with the web). The day count is `days_until_expiration`.",
                              "example": "No Expiry"
                            },
                            "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/..."
                        },
                        "can_share_outside": {
                          "type": "boolean",
                          "description": "Whether the learner may share this certificate outside the company (POST /moment_shares, subject_type certificate) — its card and its verification link. False while the company's Shared moments switch is off, or for a token without write:training.",
                          "example": true
                        }
                      }
                    },
                    "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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, learning\npaths AND outside completions (a Practice Hub certifiable pass, a course the\ncompany's other learning system reported done) merged, newest completion\nfirst — 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\nAn `external` row is an outside completion: its `id` is that record's own\n(a different table from course and path ids), `score` is the score the\nrecord carries, `credits`, `version_label`, `certificate_id` and `subject`\nare null, and `title`, `provider` and `certificate_url` name it. Course and\npath rows omit those three keys.\n\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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 (an `external` row: the outside record's id).",
                            "example": 22750
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "course",
                              "path",
                              "external"
                            ],
                            "example": "course"
                          },
                          "title": {
                            "type": "string",
                            "description": "`external` rows only — the completed training's name.",
                            "example": "Handling an angry customer"
                          },
                          "provider": {
                            "type": "string",
                            "nullable": true,
                            "description": "`external` rows only — where it was done (Practice Hub, the other learning system).",
                            "example": "Practice Hub"
                          },
                          "certificate_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "`external` rows only — a web address for the certificate, when the record has one.",
                            "example": null
                          },
                          "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
                      }
                    },
                    "download_url": {
                      "type": "string",
                      "description": "Absolute URL of `GET /training/my_records/transcript/download` with\nTHIS response's `range` / `year` applied, so the PDF a client fetches\nalways matches the rows on screen. Fetch it with the same bearer\ntoken; it answers `application/pdf` with `Content-Disposition:\nattachment`. Never hardcode the path — read it from here.\n",
                      "example": "https://acme.workforce.mangoapps.com/api/v1/training/my_records/transcript/download?range=year&year=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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/training/my_records/transcript/download": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "My Records — transcript PDF",
        "description": "The learner's transcript as the branded PDF — the document the web's\n\"Download PDF\" button serves (`Apps::Training::Learner::TranscriptController#download`),\nAPI-authenticated. Generated per request from the same loader as\n`GET /training/my_records/transcript` with the same `range` / `year`\nfilter, so the rows and the \"Total credits\" line match that payload's\n`entries` and `summary.total_ceu`. Unlike a certificate the transcript is\nnot a stored blob, so there is no signed blob URL: call this endpoint,\nideally via the `download_url` the transcript payload advertises.\n\nAnswers `application/pdf` with `Content-Disposition: attachment;\nfilename=\"transcript-<learner-name>.pdf\"` — the attachment disposition\nis what hands the bytes to a native downloader / share sheet. A learner\nwith no completions still gets a valid PDF with an empty table, matching\nthe web. The caller can only ever obtain their OWN transcript in the\ntoken's business.\n\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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. Wins over `range`; a non-scalar value is ignored.",
            "schema": {
              "type": "integer",
              "example": 2026
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript PDF",
            "headers": {
              "Content-Disposition": {
                "description": "Always `attachment`, with the learner-named filename.",
                "schema": {
                  "type": "string",
                  "example": "attachment; filename=\"transcript-marcus-delgado.pdf\""
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The Training app is not accessible to the caller (error code `access_denied`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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": {
                    "version_brief": {
                      "$ref": "#/components/schemas/TrainingVersionBrief"
                    },
                    "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/..."
                        },
                        "hero_image_origin": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "video_frame",
                            "source",
                            null
                          ],
                          "description": "Where the hero image came from: video_frame (a still taken from the course's video), source (the cover its publisher sent), or null (uploaded by a person, or no image). The web shows a non-null origin on cards but gives it way to the category colour band on the detail header.",
                          "example": "video_frame"
                        },
                        "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 5m"
                        },
                        "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/lessons (the outline LOCKED, so a learner can judge the course before enrolling).\n\n**This also governs the content blocks.** `lessons` and `qa` are null unless `tabs` names them, mirroring the web, which builds the same allowlist server-side and then clamps the requested tab to it — so `?tab=qa` on a self-paced course you are not enrolled in lands back on About. Branch on `tabs`, not on the presence of the blocks.",
                          "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",
                                "fulfilled"
                              ],
                              "example": "in_progress"
                            },
                            "status_key": {
                              "type": "string",
                              "enum": [
                                "not_started",
                                "in_progress",
                                "completed",
                                "registered"
                              ],
                              "example": "in_progress"
                            },
                            "status_label": {
                              "type": "string",
                              "description": "Display label — the badge word the web and phone course pages print (In Progress / Not Started / Overdue / Completed / Registered / Cancelled). An open enrollment past its due date reads Overdue here while status_key keeps its own enum (not_started or in_progress).",
                              "example": "In Progress"
                            },
                            "progress_percentage": {
                              "type": "integer",
                              "example": 50
                            },
                            "completed_lessons_count": {
                              "type": "integer",
                              "example": 3,
                              "description": "Completed lessons of `total_lessons`."
                            },
                            "total_lessons": {
                              "type": "integer",
                              "example": 5,
                              "description": "The lessons `progress_percentage` is computed from — required and playable (every playable lesson on a course with no required lesson); `lessons_count` is the whole outline. A completed enrollment reports `completed_lessons_count` equal to it."
                            },
                            "required_lessons_only": {
                              "type": "boolean",
                              "example": true,
                              "description": "True when `total_lessons` leaves an optional or unplayable lesson out."
                            },
                            "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",
                          "nullable": true,
                          "description": "The outline the caller may see: an enrolled learner gets the version they are PINNED to; a caller who is not enrolled gets the currently published version. The same rows the Lessons tab endpoint returns.\n\n**A caller who is not enrolled gets the outline LOCKED** — every row `locked: true`, `current: false`, `completed: false` and `web_view_url: null`: titles, types and durations, nothing that opens a lesson, as the web Lessons tab and the `/m/` course page show it. Lesson bodies stay enrollment-gated (`GET .../lessons/{lesson_id}/content` answers 422 `not_enrolled`).\n\n**NULL unless `tabs` includes `lessons`** — i.e. null for every instructor-led course (which has a Sessions tab instead). Null means WITHHELD, not empty.",
                          "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"
                        },
                        "class_checklist": {
                          "type": "object",
                          "nullable": true,
                          "description": "Instructor-led only: the course's sign-off checklist card, as the web About tab draws it — only the caller's own attempt, and counts only (the items and their signers are the signers' `/training/checklists` endpoints). Null when the course has no checklist, on a self-paced course, and for a finished attempt that was never asked for one.",
                          "required": [
                            "lesson_id",
                            "title",
                            "passing",
                            "total",
                            "state",
                            "state_label"
                          ],
                          "properties": {
                            "lesson_id": {
                              "type": "integer",
                              "description": "The checklist lesson — the edition the caller is being signed on.",
                              "example": 812
                            },
                            "title": {
                              "type": "string",
                              "example": "Sign-off checklist"
                            },
                            "passing": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Items holding a current Met or N/A. Null until the caller's attempt holds the checklist.",
                              "example": 3
                            },
                            "total": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Items on the checklist. Null until the caller's attempt holds the checklist.",
                              "example": 5
                            },
                            "state": {
                              "type": "string",
                              "enum": [
                                "browsing",
                                "both",
                                "attended",
                                "signed",
                                "done"
                              ],
                              "description": "`browsing` not on the course yet; `both` waiting on attendance and signatures; `attended` attended, waiting on signatures; `signed` every item signed, waiting on attendance; `done` the course is complete."
                            },
                            "state_label": {
                              "type": "string",
                              "description": "The sentence the web card prints for `state`.",
                              "example": "You attended. The course completes when every item is signed."
                            }
                          }
                        },
                        "qa": {
                          "type": "object",
                          "nullable": true,
                          "description": "The Q&A tab's badge counts. **NULL unless `tabs` includes `qa`** — i.e. null for a self-paced course the caller is not enrolled in. Always present for an instructor-led course, matching the web, which offers Q&A on every ILT course and only on an ENROLLED self-paced one.",
                          "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
                        },
                        "can_share": {
                          "type": "boolean",
                          "description": "May the caller share this course (POST /shares): anyone who can open it, with a `write:shares` token.",
                          "example": true
                        },
                        "my_review_id": {
                          "type": "integer",
                          "nullable": true,
                          "example": null
                        },
                        "cta": {
                          "$ref": "#/components/schemas/TrainingDetailCta"
                        },
                        "secondary_cta": {
                          "type": "object",
                          "nullable": true,
                          "description": "A second action to draw beside `cta`, or null. Today it is set only on a finished self-paced course that issued a certificate and has at least one lesson: `cta` is `view_certificate`, and this is `review`, reopening the course from its first lesson (`lesson_id`, a row of `lessons[]`) — the web course page's Review course beside View Certificate. Null on an instructor-led course, while browsing, before the course is finished, and on a finished course with no certificate (there `cta` itself is `review`). A separate field, so a client reading only `cta` is unaffected.",
                          "required": [
                            "action",
                            "lesson_id"
                          ],
                          "properties": {
                            "action": {
                              "type": "string",
                              "enum": [
                                "review"
                              ],
                              "example": "review"
                            },
                            "lesson_id": {
                              "type": "integer",
                              "description": "The lesson to open — the course's first.",
                              "example": 130
                            }
                          }
                        }
                      }
                    },
                    "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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
                        },
                        "hero_image_origin": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "video_frame",
                            "source",
                            null
                          ],
                          "description": "Where the hero image came from: video_frame (a still taken from the course's video), source (the cover its publisher sent), or null (uploaded by a person, or no image). The web shows a non-null origin on cards but gives it way to the category colour band on the detail header.",
                          "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",
                              "description": "Display label — the badge word the web and phone path pages print (In Progress / Not Started / Overdue / Completed / Cancelled). An open path enrollment past its due date reads Overdue here while status_key keeps its own enum (not_started or in_progress).",
                              "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
                            },
                            "courses_completed_before_joining": {
                              "type": "integer",
                              "description": "How many of the path's courses count toward this attempt from work the learner finished before joining it (prior completions are credited), which is why a path can read part-way or complete on arrival. 0 for a force-retake attempt.",
                              "example": 0
                            }
                          }
                        },
                        "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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}`) inlines this outline.\nThis exists so a client can open or REFRESH just the\ntab — after finishing a lesson, most obviously — without refetching a detail\npayload that also carries sessions, prerequisites, custom fields, reviews\nand the CTA. The rows are produced by the same serializer, so the two can\nnever disagree about a row.\n\nA CALLER WHO IS NOT ENROLLED gets the outline too, as the detail does: 200\nwith `enrolled: false`, every row `locked: true` with `web_view_url: null`\n— titles, types and durations only, so a learner can judge the course\nbefore enrolling. Lesson BODIES remain enrollment-gated:\n`GET .../lessons/{lesson_id}/content` answers 422 `not_enrolled`.\n\nBranch on the detail's `tabs` for which tabs to draw: an instructor-led\ncourse has no Lessons tab even though this endpoint answers for it.\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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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,
                              "description": "The rows of this list — the whole outline the tab heads. The count `progress_percentage` is computed from is the course detail's `enrollment` pair (with `required_lessons_only`)."
                            }
                          }
                        },
                        "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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 — it asks `read:training`, 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"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "can_share": {
                                  "type": "boolean",
                                  "description": "Whether POST /shares takes this lesson from the caller (a text, video or document lesson of the course's current version, a token with `write:shares`) — offer \"Share a takeaway\" exactly then. Draft the note with POST /shares/suggest_note."
                                }
                              }
                            }
                          ],
                          "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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."
          },
          "422": {
            "description": "`not_enrolled` — the caller holds no enrollment on the course. `prerequisites_not_met` — the course has a required prerequisite the caller has not finished; `details.missing_prerequisites` names each `{ id, title }`. The web refuses the lesson the same way."
          }
        }
      }
    },
    "/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* LEGACY 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  Use the new `resume_position` with `base_revision` and a UUID\n  `operation_id` for an exact cursor that can intentionally seek backwards.\n  Never replace an unacknowledged operation with a new one; replay it\n  first, then save the newer cursor against the acknowledged revision.\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`); the token lacks `write:training` (`insufficient_permissions`); or `enrollment_cancelled` — the caller's enrollment in the course was cancelled, so its lessons take no progress (the web and the phone refuse the same way, in the same words)."
          },
          "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 current enrollment on the course, so there is nothing to record progress against. Enroll first (`POST /training/courses/{id}/enroll`). A COMPLETED enrollment is accepted (an optional lesson may still be marked); a cancelled one is the 403 above.\n\n`prerequisites_not_met` — the course has a required prerequisite the caller has not finished (an admin can add one after enrolling them); `details.missing_prerequisites` names each `{ id, title }`. The web refuses the lesson the same way.\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` / `prerequisites_not_met` 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",
                                  "enrollment_cancelled",
                                  "prerequisites_not_met",
                                  "invalid_progress",
                                  "progress_failed"
                                ],
                                "description": "Present only when `ok` is false. The first six are PERMANENT — drop the queue entry (`enrollment_cancelled`: the learner's enrollment was cancelled; `prerequisites_not_met`: the course has a required prerequisite the learner has not finished). `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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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."
          },
          "422": {
            "description": "`prerequisites_not_met` — the course has a required prerequisite the caller has not finished; `details.missing_prerequisites` names each `{ id, title }`. Starting an attempt answers the same."
          }
        }
      }
    },
    "/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. `enrollment_cancelled` — the caller's enrollment in the course was cancelled, so its quiz takes no attempt."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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/admin/quiz_attempts/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read one graded quiz attempt (Training admin)",
        "description": "The result of one quiz attempt, anyone's, for a system that follows the\n`training.assessment_failed` webhook event (its `read_path` is this\nendpoint). Scores and counts only — never a question or an answer.\n`GET /training/quiz_attempts/{id}` is the learner's own player and serves\nnobody else.\n\n**Who:** Training administrators (account administrators, HR\nadministrators, Training app administrators). Anyone else gets 403.\n\n**Required Scopes:** `read:training`\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Quiz attempt ID (`quiz_attempt_id` in the event)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The attempt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attempt"
                  ],
                  "properties": {
                    "attempt": {
                      "type": "object",
                      "required": [
                        "id",
                        "user_id",
                        "training_quiz_id",
                        "attempt_number",
                        "state",
                        "failed_attempts"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer"
                        },
                        "training_quiz_id": {
                          "type": "integer"
                        },
                        "training_enrollment_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "training_course_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "attempt_number": {
                          "type": "integer"
                        },
                        "state": {
                          "type": "string"
                        },
                        "passed": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "score_percentage": {
                          "type": "number",
                          "nullable": true
                        },
                        "points_earned": {
                          "type": "number",
                          "nullable": true
                        },
                        "total_points": {
                          "type": "number",
                          "nullable": true
                        },
                        "submitted_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "time_taken_seconds": {
                          "type": "integer",
                          "nullable": true
                        },
                        "failed_attempts": {
                          "type": "integer",
                          "description": "This person's failed attempts at the quiz in the same enrollment."
                        },
                        "max_attempts": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `read:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "404": {
            "description": "No such attempt in this business"
          }
        }
      }
    },
    "/training/courses/{course_id}/quiz_attempts/{attempt_id}/review_recap": {
      "post": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "An AI recap of the lessons behind a failed attempt",
        "description": "A short reminder from the lessons linked to what the learner missed. It\nreads lesson text only, never the quiz. Offer it only when the results\nendpoint's `review.recap_available` is true — the same rule this call\nrefuses on. Requires read:training and write:chat. Same turn contract\nas …/lessons/{lesson_id}/help/{kind}; `kind` is `review`.\n",
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "attempt_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enrollment_id": {
                    "type": "integer",
                    "description": "The course attempt the results belong to. When sent and it is no longer the current one, the recap is refused."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Turn queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonHelpTurn"
                }
              }
            }
          },
          "403": {
            "description": "Missing token scope, or no Ask AI access (`app_disabled`)"
          },
          "422": {
            "description": "`help_unavailable` — no recap for this attempt: it passed or is still open, no missed question links a lesson the learner may open, or the Training assistant is off for them (`review.recap_available` is false)."
          },
          "429": {
            "description": "Ask AI rate limit or monthly budget reached"
          }
        }
      }
    },
    "/training/quiz_attempts/{id}/review_complete": {
      "post": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Say you went back over what you missed",
        "description": "Records that the learner reviewed a failed attempt, which lifts the\nquiz's require-review gate. Their own finished attempts only. The\nresults endpoint's `review` says whether one is asked for\n(`review.required`). Repeating the call is safe: the first time is kept.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The review is recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attempt_id": {
                      "type": "integer",
                      "example": 9014
                    },
                    "review_completed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Attempt not found"
          }
        }
      }
    },
    "/training/course_manifests": {
      "post": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Create a draft course from a manifest (Training admin)",
        "description": "Creates one new, unpublished draft course. It never changes an existing\ncourse, and sending the same manifest twice creates two drafts unless\nthe request carries an `Idempotency-Key` header. When anything is wrong\nnothing is created, and `error.details.problems` lists every problem\nwith the field it is about. A course holds at most 200 lessons and\n2,000 questions here; a larger one needs a ZIP.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "manifest"
                ],
                "properties": {
                  "manifest": {
                    "type": "object",
                    "description": "The course, as GET /training/course_manifests/guide describes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft course",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "course": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        },
                        "published": {
                          "type": "boolean"
                        },
                        "url": {
                          "type": "string"
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only a Training administrator can create a course this way"
          },
          "400": {
            "description": "The body is not valid JSON"
          },
          "422": {
            "description": "The body has no `manifest` object, or the manifest has problems. Nothing was created"
          }
        }
      }
    },
    "/training/course_manifests/check": {
      "post": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Check a course manifest without keeping anything (Training admin)",
        "description": "Validates the manifest against the schema and every rule, then tries\nthe import and rolls it back. Answers 200 whether or not the manifest\nis valid, with every problem and the field it is about.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "manifest"
                ],
                "properties": {
                  "manifest": {
                    "type": "object",
                    "description": "The course, as GET /training/course_manifests/guide describes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The result of the check",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "problems": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string",
                            "description": "A JSON pointer to the field"
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string"
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only a Training administrator can create a course this way"
          },
          "400": {
            "description": "The body is not valid JSON"
          },
          "422": {
            "description": "The body has no `manifest` object"
          }
        }
      }
    },
    "/training/course_manifests/guide": {
      "get": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Read how to write a course as JSON (Training admin)",
        "description": "Returns the JSON schema for a course manifest, a worked example with\nevery question type a manifest can hold, and the rules the schema\ncannot state.\n",
        "responses": {
          "200": {
            "description": "The schema, example and guide",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schema": {
                      "type": "object"
                    },
                    "example": {
                      "type": "object"
                    },
                    "guide": {
                      "type": "string",
                      "description": "The rules, as Markdown"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only a Training administrator can create a course this way"
          }
        }
      }
    },
    "/training/managed_courses/{key}": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "The outside source's own name for the course — lower-case letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, at most 100 characters (`app-training:sites`). One course per key in a business.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._:-]{0,99}$"
          }
        }
      ],
      "get": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Read a course that is published from outside the app (Training admin)",
        "description": "A course whose lessons are produced outside Training — films with their\nquizzes — and published here again whenever they change. `key` is the\nname the outside source gives the course; it never changes, whatever\nthe course is later called in Training.\n\nAnswers what Training holds for that key now: the course, its versions,\nand the lessons the next publish would start from, each with the MD5\nchecksum (base64) of its files, so a sender can skip a file that has not\nchanged.\n\n**Who:** Training administrators. Anyone else gets 403.\n\n**Required Scopes:** `read:training`\n\n**Credential:** a service account token created by a Training\nadministrator, sent with its secret (`X-API-Secret`). It acts as the\nperson who created it. A token a person makes for themselves cannot\nhold `read:training` or `write:training`, so it is refused here and on\nthe three writes below.\n",
        "responses": {
          "200": {
            "description": "The course as it stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagedCourse"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `read:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "404": {
            "description": "No course is kept under this key in this business (`not_found`)"
          },
          "422": {
            "description": "The key is not a usable key (`invalid_key`)"
          }
        }
      },
      "put": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Open a course for publishing from outside the app (Training admin)",
        "description": "The first step of a publish. Finds the course kept under `key`, or\ncreates it, and makes sure it has a draft version of its own to receive\nlessons. Safe to repeat.\n\nA new course is created from the fields sent (a `title` is required\nthen) and stays unpublished until `POST …/publish`. For a course that\nalready exists the fields are ignored here: its title, description,\ncategory, level and objectives change only when a publish succeeds, so\nnothing a learner sees changes part-way through.\n\nRefused, with nothing changed, when:\n* another course in the business already has that title and was not created from this source — `title_taken`;\n* someone has a version of the course open as a draft in Training — `draft_in_progress`;\n* an earlier publish stopped part-way, and a lesson someone added by hand has since been edited on the live version — `draft_stale` (discard that draft in Training, then send this again);\n* an administrator unpublished or archived the course — `course_unpublished`, `course_archived`;\n* a lesson of the live version could not be copied into the new draft — `lessons_not_copied`.\n\n**Who:** Training administrators.\n\n**Required Scopes:** `write:training`\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagedCourseFields"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The course, with its draft open",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagedCourse"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `write:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "409": {
            "description": "`title_taken`, `draft_in_progress`, `draft_stale`, `course_unpublished`, `course_archived`, `lessons_not_copied`, or `busy` (another publish of this course is running — send it again)"
          },
          "422": {
            "description": "`invalid_key`, or `not_saved` with the reason (a new course without a title, for one)"
          }
        }
      }
    },
    "/training/managed_courses/{key}/lessons/{lesson_key}": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "The outside source's own name for the course — lower-case letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, at most 100 characters (`app-training:sites`). One course per key in a business.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._:-]{0,99}$"
          }
        },
        {
          "name": "lesson_key",
          "in": "path",
          "required": true,
          "description": "The source's own name for the lesson. Same form as the course key. A lesson's quiz is kept by Training under `<lesson_key>/quiz`; a source never sends that key.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._:-]{0,99}$"
          }
        }
      ],
      "put": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Send one lesson of a course published from outside the app (Training admin)",
        "description": "Creates or updates one lesson in the draft that `PUT …/managed_courses/{key}`\nopened, and the quiz that follows it. One lesson per request, with its\nfiles as multipart beside the lesson's details. Safe to repeat: a file\nwhose bytes Training already holds is not stored twice, and may simply\nbe left out.\n\nThe lesson is the source's lesson as it is now — a description, a\nchapter list or a quiz that is no longer sent is removed from it.\n\n* A **video** lesson needs its `video` file the first time, and its\n  `duration_seconds` every time. A transcript sent is the transcript\n  kept, and the video is not transcribed again; a new video sent\n  without one is transcribed by Training like any upload. A new video\n  without `captions` drops the captions of the video it replaces.\n* A **document** lesson needs its `document` file the first time.\n* A lesson keeps the kind it was first sent as (`kind_changed`).\n* A quiz whose questions differ from the one the lesson has becomes a\n  new quiz, with the attempt limit and other settings of the one it\n  replaces; learners still on an earlier version of the course keep the\n  questions that match what they watched.\n\nA video may be up to 200 MB; a document must be under 50 MB. What is\nwrong with the lesson's own details is said before any file is stored.\n\n**Who:** Training administrators.\n\n**Required Scopes:** `write:training`\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "lesson"
                ],
                "properties": {
                  "lesson": {
                    "type": "string",
                    "description": "The lesson's details, as a JSON string — see `ManagedCourseLessonInput`."
                  },
                  "video": {
                    "type": "string",
                    "format": "binary",
                    "description": "MP4, WebM or QuickTime. For a `video` lesson."
                  },
                  "captions": {
                    "type": "string",
                    "format": "binary",
                    "description": "WebVTT or SubRip captions for the video."
                  },
                  "document": {
                    "type": "string",
                    "format": "binary",
                    "description": "For a `document` lesson — a PDF, for one."
                  }
                }
              },
              "encoding": {
                "lesson": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The lesson as Training now holds it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "lesson"
                  ],
                  "properties": {
                    "lesson": {
                      "$ref": "#/components/schemas/ManagedCourseLesson"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `write:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "404": {
            "description": "No course is kept under this key yet — open it first (`not_found`)"
          },
          "409": {
            "description": "`not_open` (the course has no draft — open it first), `kind_changed`, `draft_in_progress`, `draft_stale`, `course_unpublished`, `course_archived`, or `busy`"
          },
          "422": {
            "description": "`invalid_lesson`, `invalid_key`, `invalid_kind`, `invalid_quiz`, `video_required`, `document_required`, `file_refused` (wrong type or too large), or `not_saved` with the reason"
          },
          "503": {
            "description": "File storage could not be reached — send the lesson again (`storage_unavailable`)"
          }
        }
      }
    },
    "/training/managed_courses/{key}/publish": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "The outside source's own name for the course — lower-case letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, at most 100 characters (`app-training:sites`). One course per key in a business.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._:-]{0,99}$"
          }
        }
      ],
      "post": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Publish a course sent from outside the app (Training admin)",
        "description": "The last step. Puts the draft's lessons in the order given — each\nfollowed by its quiz — removes the lessons the source no longer names,\nbrings the course's own title, description, category, level and\nobjectives up to date from the fields sent, and publishes the draft as\nthe course's new version.\n\nWhen the draft is the same as the live version, nothing is published:\nthe draft is discarded and `unchanged` is true. Sending the same\npackage twice therefore never adds a version.\n\nA lesson someone added to the course by hand in Training is kept, after\nthe lesson it followed.\n\nAll of it happens together or not at all: when this is refused, the\nlive version is as it was.\n\nWith `announce`, people are then told the course is in the catalog —\nthe course page's **Tell people**: one in-app notice (\"New course:\n<title>\", or \"Updated course: <title>\" for a later version), and an\nemail too when `email` is true (people without an address get the\nin-app notice only); with `feed` true, a post on the News Feed as well,\nseen only by the same audience. Only people who can see the course in\nthe Training catalog are told, never one who already has it, and nobody\ntwice about the same version, so sending the same publish again tells\nnobody new and posts nothing new. Nothing is assigned. The publish\nstands whatever `announcement` answers.\n\n**Who:** Training administrators.\n\n**Required Scopes:** `write:training`\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ManagedCourseFields"
                  },
                  {
                    "type": "object",
                    "required": [
                      "lessons"
                    ],
                    "properties": {
                      "lessons": {
                        "type": "array",
                        "description": "The keys of the course's lessons, in order. Every key must have been sent to this draft, or already be in it.",
                        "items": {
                          "type": "string"
                        }
                      },
                      "announce": {
                        "description": "Tell people about the course once it is live. Left out, null or false, nobody is told and no `announcement` is answered; `true` is everyone, in the app only.",
                        "oneOf": [
                          {
                            "type": "boolean"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "audience": {
                                "description": "`everyone` (the default) — everyone who can see the course in the catalog — or the people to tell, of whom only those who can see it are told.",
                                "oneOf": [
                                  {
                                    "type": "string",
                                    "enum": [
                                      "everyone"
                                    ]
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "group_ids": {
                                        "type": "array",
                                        "description": "Saved groups (notification recipient groups) in this business.",
                                        "items": {
                                          "type": "integer"
                                        }
                                      },
                                      "user_ids": {
                                        "type": "array",
                                        "description": "People in this business.",
                                        "items": {
                                          "type": "integer"
                                        }
                                      },
                                      "criteria": {
                                        "type": "array",
                                        "description": "The audience picker's criteria, each adding people: `{\"type\": \"department\", \"ids\": [..]}`, `{\"type\": \"location\", \"ids\": [..]}`, `{\"type\": \"job_title\", \"titles\": [..]}`, `{\"type\": \"role\", \"roles\": [..]}`.",
                                        "items": {
                                          "type": "object"
                                        }
                                      }
                                    }
                                  }
                                ]
                              },
                              "email": {
                                "type": "boolean",
                                "default": false,
                                "description": "Send the notice by email as well as in the app (`\"true\"` / `\"false\"` are read the same)."
                              },
                              "notify": {
                                "type": "boolean",
                                "default": true,
                                "description": "Send the in-app notice. False with `feed` true posts on the feed only."
                              },
                              "feed": {
                                "type": "boolean",
                                "default": false,
                                "description": "Post it on the News Feed for the same audience — one post per live version, taken down if the course leaves the catalog. When the company's feed does not take Training posts it is refused with `feed_unavailable` if nothing else was asked; with `notify`, the notice goes and `feed.error` says why there is no post."
                              },
                              "note": {
                                "type": "string",
                                "maxLength": 500,
                                "description": "The administrator's own line, on the feed post, in the notice and in the email. Refused with `note_too_long` past 500 characters."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The course after the publish",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ManagedCourse"
                    },
                    {
                      "type": "object",
                      "required": [
                        "unchanged",
                        "course_updated"
                      ],
                      "properties": {
                        "unchanged": {
                          "type": "boolean",
                          "description": "True when nothing differed from the live version — no version was published and the course's own fields were already up to date."
                        },
                        "course_updated": {
                          "type": "boolean",
                          "description": "True when the course's own title, description, category, level or objectives changed."
                        },
                        "announcement": {
                          "type": "object",
                          "description": "Only when `announce` was sent. `queued: true` with how many people it was sent to (someone who muted New training courses is counted and not told), or `queued: false` with why not: `not_in_catalog` (the course is assign-only, or self-enrollment is off for everyone in Training Settings), `sample_course` (sample data never sends a notice), `no_audience`, `nobody_to_tell` (everyone in the audience was told already or has the course), `no_channel` (`notify` and `feed` both false), `feed_unavailable` (only `feed` asked, and the company's feed does not take Training posts — with `notify` too, the notice goes and `feed.error` says why there is no post), `no_readers` and `save_failed` (only `feed` asked: nobody in the audience can see the course, or the post could not be saved), `note_too_long`, `not_shown` (a MangoApps course this company does not show), `invalid_announce`, or `announce_failed` (the course is published; use Tell people on the course page).",
                          "properties": {
                            "queued": {
                              "type": "boolean"
                            },
                            "recipients": {
                              "type": "integer"
                            },
                            "notify": {
                              "type": "boolean"
                            },
                            "email": {
                              "type": "boolean"
                            },
                            "audience": {
                              "type": "string",
                              "enum": [
                                "everyone",
                                "chosen"
                              ]
                            },
                            "feed": {
                              "description": "Only when `feed` was asked for. `posted` false with `error` when the notice went and the post did not.",
                              "nullable": true,
                              "type": "object",
                              "properties": {
                                "posted": {
                                  "type": "boolean"
                                },
                                "already": {
                                  "type": "boolean",
                                  "description": "The feed already had its post about this version."
                                },
                                "feed_id": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "error": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            },
                            "error_code": {
                              "type": "string"
                            },
                            "message": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `write:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "404": {
            "description": "No course is kept under this key (`not_found`)"
          },
          "409": {
            "description": "`not_open`, `draft_in_progress`, `draft_stale`, `course_unpublished`, `course_archived`, `approval_required` (the business asks for approval before a course is published — approve it in Training, then send this again), or `busy`"
          },
          "422": {
            "description": "`missing_order` (no `lessons`), `invalid_key` (the order names something that is not a lesson's own key — a lesson's quiz follows it by itself), `missing_lessons` (a key in the order was never sent), `no_lessons`, or `not_saved` with the reason"
          }
        }
      }
    },
    "/training/managed_courses/{key}/cover": {
      "parameters": [
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "The outside source's own name for the course — lower-case letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, at most 100 characters (`app-training:sites`). One course per key in a business.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9._:-]{0,99}$"
          }
        }
      ],
      "put": {
        "tags": [
          "Training"
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "summary": "Set the cover image of a course sent from outside the app (Training admin)",
        "description": "The image the course's catalog card and page header show. It is not\npart of a version — learners see it as soon as it is saved — so a\nsource sends it after `publish`, not while a draft is held.\n\nAn image a Training administrator chose for the course in Training is\ntheirs: it is kept, nothing is refused, and `cover` says `kept`.\nSending the cover the course already has changes nothing; the course's\n`cover_checksum` is the checksum of the one this source sent last.\n\nA course with no cover shows a still from its first video instead.\n\n**Who:** Training administrators.\n\n**Required Scopes:** `write:training`\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "PNG, JPEG or WebP, under 10 MB. A 16:9 frame fills the card without cropping."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The course, and what happened to the cover",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ManagedCourse"
                    },
                    {
                      "type": "object",
                      "required": [
                        "cover"
                      ],
                      "properties": {
                        "cover": {
                          "type": "string",
                          "enum": [
                            "set",
                            "kept"
                          ],
                          "description": "`set` — the course shows this image; `kept` — an administrator chose the course's image in Training, and it stays."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `write:training`, Training is not available to the caller, or the caller is not a Training administrator"
          },
          "404": {
            "description": "No course is kept under this key yet — open it first (`not_found`)"
          },
          "409": {
            "description": "Someone else is changing this course right now (`busy`)"
          },
          "422": {
            "description": "`invalid_cover` (no `image` file), `file_refused` (wrong type or too large), or `not_saved` with the reason"
          },
          "503": {
            "description": "File storage could not be reached — send it again (`storage_unavailable`)"
          }
        }
      }
    },
    "/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": {
                  "base_revision": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "The attempt answers_revision last acknowledged. Stale revisions return 409."
                  },
                  "operation_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Stable for the exact full snapshot until acknowledged; reuse with changed content is rejected."
                  },
                  "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 (`access_denied`); the token lacks `write:training` (`insufficient_permissions`, #7183); or `enrollment_cancelled` — the enrollment this attempt belongs to was cancelled, so nothing was saved. The player and the results stay readable."
          },
          "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.\n\n`attempt_timed_out` — this answer arrived after the time limit (plus the grace) ran out; it was not saved, and the attempt has just been graded from the answers saved in time. `error.details.attempt_id` names it — take the learner to its 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"
                  },
                  "question_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "For a question-pool quiz: the `question_ids` the offline package carried. Honoured when it is a draw the edition could have made (its own questions, every required one, the right number); otherwise the attempt is scored on a draw made at replay."
                  },
                  "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/TrainingQuizCourseState"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`attempt_blocked` — attempts used up, already passed, or cooling down; nothing created. `enrollment_cancelled` — the caller's enrollment in the course was cancelled; nothing created. Or `insufficient_permissions` — the token lacks `write:training` (#7183)."
          },
          "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 (`access_denied`); the token lacks `write:training` (`insufficient_permissions`, #7183); or `enrollment_cancelled` — the enrollment this attempt belongs to was cancelled, so nothing was saved. The player and the results stay readable."
          },
          "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 not scored, no staff screen shows them, and they are\nexcluded from BOTH sides of the score, so `correct_count` /\n`auto_graded_count` / `pending_review_count` are given rather than a single\n\"N of M correct\" that would silently count a question nobody grades. Compose\nthe sentence from figures that add up, and render a `pending_review` row as\n\"Not scored\" — never as incorrect, and never as awaiting a review (no\ngrading step exists). A quiz of written answers only completes passed with\n`score_percentage: null` — render \"Not scored\", not \"0%\".\n\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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"
                    },
                    "review": {
                      "$ref": "#/components/schemas/TrainingQuizMissedReview"
                    },
                    "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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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 — and only to a credential holding\n`destructive:training`, the scope the restart asks (a signed-in session\nalways does). 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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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."
          },
          "422": {
            "description": "`prerequisites_not_met` — the course has a required prerequisite the caller has not finished; `details.missing_prerequisites` names each `{ id, title }`."
          }
        }
      }
    },
    "/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 `destructive:training` — it wipes the module's resume state, a\ndelete-level act (#7183, #6721).\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 `destructive: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}/lessons/{lesson_id}/practice": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Practice lesson card (Practice Hub roleplay)",
        "description": "Everything a learner sees BEFORE starting the AI roleplay behind a\n`practice` lesson: the scenario's difficulty, pass threshold and estimated\ntime, the persona they will talk to, whether the practice can be played at\nall, and their standing on it. The native mirror of the web launch card.\n\n**Draw the Start control from `can_start`.** It is `false` when the\nlearner is not enrolled (`enrolled: false`) or the practice is\nunavailable (`unavailable_reason`). Availability is the SAME question the\nstart asks, so a client that honours `can_start` never sees the start's\n422s.\n\n| `unavailable_reason` | meaning                                                    |\n|----------------------|------------------------------------------------------------|\n| `no_scenario`        | the lesson's scenario no longer exists                     |\n| `unpublished`        | the scenario is no longer published (archived, back in review) |\n| `app_disabled`       | Practice Hub is not enabled for the organization           |\n| `no_persona`         | the scenario has no persona to talk to                     |\n\nAn unavailable practice lesson — for any of the four reasons — is left\nout of the course's required count, so it never strands a learner below\n100%.\n\n**Access comes from the course.** A learner enrolled in the course can take\nits practice lesson whether or not they are in Practice Hub's own audience\ngroups — the web runtime admits them the same way. Only the organization\nhaving Practice Hub enabled is required (`app_disabled` otherwise).\n\n`open_attempt_id` is an in-progress attempt to RESUME — starting again\nreturns it rather than a new one. Counts (`attempts_count`, `best_score`,\n`last_attempt`) cover only the attempts launched from THIS lesson on the\nlearner's current enrollment at the lesson's CURRENT scenario, never\ndiscarded ones and never their Practice Hub library practice.\n\nRenders for a learner who is not enrolled too (`enrolled: false`), from the\ncourse's browse version, so the outline can show the card before enrolment.\n\n**Required Scopes:** `read:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 773
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "description": "The practice lesson's id. A lesson of any other content type 404s — a course version can carry several practice lessons, so the lesson id says which one is meant.",
            "schema": {
              "type": "integer",
              "example": 1349
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Practice card retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingPracticeCard"
                    },
                    {
                      "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 token lacks `read:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "`not_found` — a course outside the caller's business, an unpublished or archived course, a lesson not in the learner's version, or a lesson that is not a practice lesson. All answer alike."
          },
          "422": {
            "description": "`prerequisites_not_met` — the course has a required prerequisite the caller has not finished; `details.missing_prerequisites` names each `{ id, title }`."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/practice/attempts": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Start or resume a practice roleplay",
        "description": "Starts the roleplay behind a practice lesson — or hands back the learner's\nin-progress attempt for this lesson, so a double tap or a retried request\nnever mints a second run. The response is the attempt, with the persona's\nopening line already in `turns` when the scenario has one.\n\n**201** when an attempt was created, **200** when an open one was resumed;\n`resumed` says the same thing in the body.\n\nStarting marks the lesson started (course progress shows it in progress)\nand satisfies an open Practice Hub assignment for the scenario. The attempt\nis stamped with the lesson and enrollment, which is what lets a passing\nscore complete the lesson. The same service runs the web \"Start practice\"\nbutton, so the two can never disagree.\n\nTyped replies only in v1: spoken replies, live voice and persona voice\nplayback stay on the web.\n\nAccess comes from the course enrollment, as on the card: Practice Hub's\naudience groups are not asked; the organization must have Practice Hub.\n\n**Required Scopes:** `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 773
            }
          },
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1349
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Attempt created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingPracticeAttempt"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "resumed": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "200": {
            "description": "The learner's in-progress attempt, resumed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingPracticeAttempt"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "resumed": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "As for the card — `not_found`."
          },
          "422": {
            "description": "Refused, nothing written. `error.message` is written for the learner.\n\n`practice_unavailable` — `details.reason` is `no_scenario`, `unpublished` or `app_disabled` (the card's `unavailable_reason`).\n\n`no_persona` — the scenario has no persona to talk to.\n\n`not_enrolled` — the caller holds no enrollment for this course.\n\n`prerequisites_not_met` — as for the card."
          },
          "500": {
            "description": "`start_failed` — the start could not be written; nothing was saved. Safe to retry."
          }
        }
      }
    },
    "/training/practice_attempts/{id}": {
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "A practice attempt — transcript, status and scorecard",
        "description": "The roleplay chat and, once scored, its scorecard. Only the caller's own\nCOURSE-launched attempts are served here; another learner's id, a missing\none, and the caller's own Practice Hub library attempt all 404 alike.\n\n**Poll this while `processing_status` is `queued` or `scoring` and\n`scoring_stalled` is false** — the same signal the web page's reload timer\nuses. It becomes `null` with `status: \"scored\"` and a `scorecard` on\nsuccess, or `failed` when the judge gave up; re-POST `complete` to retry.\n`scoring_stalled` turns true when a run has not moved for 20 minutes:\nstop polling and offer \"Try scoring again\" (`can_complete` is then true),\nas the web pages do.\n\n`lesson_completed`, `course_status` and `course_progress_percentage` are\nread on every response, so the client can move its progress bar and footer\nthe moment a passing score lands without re-reading the outline. Only a\nPASS completes the lesson.\n\n`turns` is the conversation's tail, oldest first — at most 120 turns;\n`turns_truncated` says when earlier turns exist.\n\n**Required Scopes:** `read:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A practice attempt id the start returned (the caller's own, course-launched).",
            "schema": {
              "type": "integer",
              "example": 5012
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attempt retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingPracticeAttempt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `read:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "`not_found` — no such course-launched attempt for this caller."
          }
        }
      }
    },
    "/training/practice_attempts/{id}/messages": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Send the learner's reply and get the persona's",
        "description": "The learner's typed reply; the persona's answer is generated inside the\nrequest (one model call, the same turn the web runtime runs) and both\nturns come back. Nothing is saved when the reply fails, so the learner can\nresend the same text.\n\nOnly an attempt whose `status` is `in_progress` takes a reply.\n\n**Required Scopes:** `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A practice attempt id the start returned (the caller's own, course-launched).",
            "schema": {
              "type": "integer",
              "example": 5012
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "The learner's reply. Longer than 8,000 characters is cut there.",
                    "example": "Yesterday a forklift nearly hit someone at the blind corner by dock 3."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Both turns, learner first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "turns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrainingPracticeTurn"
                      }
                    },
                    "can_reply": {
                      "type": "boolean",
                      "example": true
                    },
                    "can_complete": {
                      "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 (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "`not_found` — no such course-launched attempt for this caller."
          },
          "409": {
            "description": "`not_in_progress` — the conversation has ended (completed, scored or abandoned); `details.status` says which. Redraw, don't retry. `practice_unavailable` (`details.reason: app_disabled`) — Practice Hub has been switched off for the organization."
          },
          "422": {
            "description": "`reply_failed` — the persona's reply could not be generated; nothing was saved, try again. `ai_limit_reached` — the organization's AI usage policy refused it; `error.message` is the policy's own sentence, show it. `blank_message` — nothing to send."
          }
        }
      }
    },
    "/training/practice_attempts/{id}/complete": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "End the roleplay and queue scoring",
        "description": "Ends the conversation and queues scoring, which runs on a background job\n(several judge calls — too slow for a request). Answers **202** with the\nattempt (`status: \"completed\"`, `processing_status: \"queued\"`); poll\n`GET /training/practice_attempts/{id}` until it is scored.\n\nA second POST while scoring is in flight queues nothing and answers 202\nagain. It is also the **retry** after `processing_status: \"failed\"`.\n\n**Idempotent while scoring is in flight** — but a run that has STALLED\n(`scoring_stalled: true`, no progress for 20 minutes) is re-queued.\n\nOn a passing score the lesson is completed and course progress moves, by\nthe same writeback the web flow uses. A failing score completes nothing;\nthe learner can start another run.\n\n**Required Scopes:** `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A practice attempt id the start returned (the caller's own, course-launched).",
            "schema": {
              "type": "integer",
              "example": 5012
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Scoring queued (or already running)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingPracticeAttempt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "`not_found` — no such course-launched attempt for this caller."
          },
          "409": {
            "description": "`already_scored` — it is scored; read the scorecard. `not_in_progress` — it was discarded and will never be scored. `practice_unavailable` (`details.reason: app_disabled`) — Practice Hub has been switched off."
          },
          "422": {
            "description": "`nothing_to_score` — the learner has not replied yet; there is nothing to grade."
          }
        }
      }
    },
    "/training/practice_attempts/{id}/discard": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Discard an in-progress roleplay",
        "description": "Throws away a run that is still in progress (`status` → `abandoned`), so a\nfalse start is never scored and never resumed — the next start creates a\nnew attempt. A finished run cannot be discarded.\n\n**Required Scopes:** `write:training`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A practice attempt id the start returned (the caller's own, course-launched).",
            "schema": {
              "type": "integer",
              "example": 5012
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Discarded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingPracticeAttempt"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `write:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "`not_found` — no such course-launched attempt for this caller."
          },
          "409": {
            "description": "`not_in_progress` — only a run still in progress can be discarded; `details.status` is its status."
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/transcript_approval": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "lesson_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Review a video transcript for lesson help",
        "description": "Requires read:training and active course content-manager access in this organization.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Source text, digest and permitted actions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lesson_id": {
                      "type": "integer"
                    },
                    "transcript": {
                      "type": "string",
                      "nullable": true
                    },
                    "digest": {
                      "type": "string"
                    },
                    "approved": {
                      "type": "boolean"
                    },
                    "can_approve": {
                      "type": "boolean"
                    },
                    "can_withdraw": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an active content manager or missing token scope"
          },
          "404": {
            "description": "Course or lesson unavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Approve the reviewed transcript as lesson-help evidence",
        "description": "Requires write:training and course content-manager access. The digest must match the reviewed transcript and current video source. AI agents cannot approve their own evidence.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "digest"
                ],
                "properties": {
                  "digest": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved; returns the same source and permission payload as GET"
          },
          "403": {
            "description": "Action or scope denied"
          },
          "409": {
            "description": "Transcript or video changed; review the current source"
          }
        }
      },
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Withdraw transcript approval",
        "description": "Requires destructive:training and active course content-manager access. Subsequent lesson help fails closed for this source.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn; returns the same source and permission payload as GET"
          },
          "403": {
            "description": "Action or scope denied"
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/help/{kind}": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "lesson_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "kind",
          "in": "path",
          "required": true,
          "description": "`explain` (alias `simple`) explains the lesson simply; `example` gives one illustrative workplace example.",
          "schema": {
            "type": "string",
            "enum": [
              "explain",
              "simple",
              "example"
            ]
          }
        }
      ],
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Ask lesson help to explain a lesson or give a work example",
        "description": "Requires read:training and write:chat, Ask AI and Training access, and a current enrollment in the course whose version holds the lesson. Only a text lesson, or a video lesson whose transcript a content manager approved, has a source to answer from; a quiz is never answered. The prompt is the server's — a client `message`, `system_context`, `app_slug`, `conversation_id` or attachment is ignored — and the turn is read-only: it changes no progress and offers no actions. Each call opens its own private thread. Subscribe to `websocket.channel` with `websocket.subscription.conversation_id` for the streamed answer, exactly as for POST /ask_ai/messages; its citations are the `sources` returned here.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enrollment_id": {
                    "type": "integer",
                    "description": "The course attempt the player opened. When sent and it is no longer the current one, the request is refused with enrollment_changed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Turn queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonHelpTurn"
                }
              }
            }
          },
          "403": {
            "description": "Missing token scope, or `unavailable` (no Training or Ask AI access)"
          },
          "404": {
            "description": "`not_found` — not enrolled, or the lesson is not in the course version this learner takes"
          },
          "409": {
            "description": "`enrollment_changed` — the course attempt changed; reopen the lesson"
          },
          "422": {
            "description": "`quiz_lesson`, `no_approved_transcript`, `help_unavailable` (no usable source, or the Training assistant is off) or `prerequisites_not_met`"
          },
          "429": {
            "description": "Ask AI rate limit or monthly budget reached (the POST /ask_ai/messages refusals)"
          },
          "503": {
            "description": "Ask AI unavailable, or paused by a security lockdown"
          }
        }
      }
    },
    "/training/courses/{course_id}/lessons/{lesson_id}/help/source": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "lesson_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "digest",
          "in": "query",
          "required": true,
          "description": "The `digest` of the cited source, from the turn's `sources`.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "enrollment_id",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Check a lesson-help citation before opening it",
        "description": "Requires read:training. The native twin of the web citation link: says whether the cited source is still the lesson as it is now (its text, or its approved transcript, unchanged) and where to open it. When `current` is false the answer was written from an older source; ask lesson help again.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether the citation is current",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "current": {
                          "type": "boolean"
                        },
                        "course_id": {
                          "type": "integer"
                        },
                        "lesson_id": {
                          "type": "integer"
                        },
                        "anchor": {
                          "type": "string",
                          "nullable": true,
                          "description": "The lesson player's source anchor (`native-lesson-source`) when current."
                        },
                        "message": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why the citation is not current."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing token scope",
            "or no Training access": null
          },
          "404": {
            "description": "Not enrolled",
            "or the lesson is not in this learner's course version": null
          }
        }
      }
    },
    "/training/courses/{course_id}/resume_recap": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Recap the learner's completed lessons",
        "description": "Requires read:training and write:chat. Summarises up to three of the learner's completed text lessons or approved-transcript video lessons in their current course version — a partial recap, never proof of mastery or a quiz answer. Same turn contract and rules as …/lessons/{lesson_id}/help/{kind}; `kind` is `recap`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enrollment_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Turn queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonHelpTurn"
                }
              }
            }
          },
          "403": {
            "description": "Missing token scope",
            "or no Ask AI access": null
          },
          "422": {
            "description": "`nothing_to_recap` — no completed lesson has a source yet; or `help_unavailable`"
          },
          "429": {
            "description": "Ask AI rate limit or monthly budget reached"
          }
        }
      }
    },
    "/training/courses/{course_id}/lesson_feedbacks": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Private lesson feedback",
        "description": "Course content managers see the course queue; learners see only their own feedback. Requires read:training. Managers without course content permission cannot read employees' private notes.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "needs_response — the web queue's default, open Unclear and Outdated reactions; or one status (new, in_review, answered, updated, closed). Omitted: every reaction.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated feedback and token-aware can_respond, can_edit_note and can_undo flags.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedbacks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrainingLessonFeedback"
                      }
                    },
                    "meta": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Training access or token scope denied"
          }
        }
      },
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Leave a private lesson reaction",
        "description": "Requires write:training, a current own enrollment, and a lesson in its pinned edition. The operation_id makes retries idempotent. Feedback does not mark the lesson complete or create a public course review.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "lesson_id",
                  "enrollment_id",
                  "reaction",
                  "operation_id"
                ],
                "properties": {
                  "lesson_id": {
                    "type": "integer"
                  },
                  "enrollment_id": {
                    "type": "integer"
                  },
                  "reaction": {
                    "type": "string",
                    "enum": [
                      "useful",
                      "unclear",
                      "outdated"
                    ]
                  },
                  "operation_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "need_example",
                      "too_fast",
                      "hard_to_understand",
                      "not_relevant",
                      "possibly_wrong",
                      "playback_or_access"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved reaction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonFeedback"
                }
              }
            }
          },
          "403": {
            "description": "Training access or token scope denied"
          },
          "200": {
            "description": "Idempotent replay of the same reaction"
          },
          "409": {
            "description": "Reused operation or enrollment changed"
          },
          "404": {
            "description": "Lesson unavailable"
          },
          "422": {
            "description": "Invalid reaction"
          }
        }
      }
    },
    "/training/courses/{course_id}/lesson_feedbacks/{id}": {
      "parameters": [
        {
          "name": "course_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Training"
        ],
        "summary": "Read private feedback and its response",
        "description": "Only the reporting employee or an active course content manager. Requires read:training.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback with allowed actions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonFeedback"
                }
              }
            }
          },
          "404": {
            "description": "Not found or not visible"
          }
        }
      },
      "patch": {
        "tags": [
          "Training"
        ],
        "summary": "Add a note or respond to lesson feedback",
        "description": "Requires write:training. The reporting employee can edit a note/reason while open. Course content managers can assign, respond, and change status. Closing also requires destructive:training. Answered, Updated and Closed require a response. Updated additionally requires the same lesson in a newer published course version. Replies notify only the reporting employee.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "reason": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "new",
                      "in_review",
                      "answered",
                      "updated",
                      "closed"
                    ]
                  },
                  "response": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "assignee_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "corrected_lesson_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingLessonFeedback"
                }
              }
            }
          },
          "403": {
            "description": "Action or scope denied"
          },
          "422": {
            "description": "Response or published correction required"
          }
        }
      },
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Undo my new feedback",
        "description": "Requires destructive:training. Only its author, while the feedback is New and unanswered.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Removed"
          },
          "403": {
            "description": "Already handled",
            "not the author": null,
            "or missing scope": null
          }
        }
      }
    },
    "/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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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`).\n`insufficient_permissions` — the token lacks `write:training` (#7183)."
          },
          "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. Someone whose reports have\nreports may also ask for `whole_line` — their direct reports and everyone\nbelow them — which is never a default. `scope` reports what was actually\napplied:\n\n- `scope.key` — `all_employees`, `direct_reports` or `whole_line`, the\n  scope in force.\n- `scope.available` — what THIS caller may ask for: `direct_reports` and\n  `all_employees` for an admin, `[\"direct_reports\"]` alone for a manager,\n  plus `whole_line` for anyone with a reporting line below their reports,\n  so a client renders the segmented control only when there is a real\n  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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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. `whole_line` — direct reports and everyone below them — is offered only to someone whose reports have reports. A manager passing `all_employees` is still served their direct reports (the role is a ceiling), and so is a caller passing `whole_line` with no line below their reports. 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",
                "whole_line"
              ]
            }
          },
          {
            "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
            }
          },
          {
            "name": "brief_kind",
            "in": "query",
            "required": false,
            "description": "One kind of the manager's action brief (`actions`) to page through instead of its top five. Unrecognised values are ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "signoffs",
                "follow_up",
                "renewals",
                "overdue"
              ]
            }
          },
          {
            "name": "brief_page",
            "in": "query",
            "required": false,
            "description": "With `brief_kind`, which 20-row page of `actions.rows`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "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`).\n`insufficient_permissions` — the token lacks `read:training` (#7183)."
          }
        }
      }
    },
    "/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, their issued certificates, and `departments` — every\ndepartment they belong to here, as the web page's subtitle lists them,\nwhere the organization's field settings show Department to the caller.\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` is LEGACY and may overlap**, unchanged for shipped apps: by raw\nstatus (`assigned` = not begun, `in_progress` = started), with\n`counts.overdue` counting outstanding rows past their due date that are\nALSO in `assigned` or `in_progress`. **`bucket_counts`** is the partition\nthe web and phone drill-ins show (Training::LearningStatus): not_started +\nin_progress + overdue is everything outstanding, an overdue course counted\nonce, as overdue.\n\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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",
                "description": "Send `enrollment_id` for a course, or `path_enrollment_id` for a learning path.",
                "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
                  },
                  "path_enrollment_id": {
                    "type": "integer",
                    "description": "One of the member's own learning-path enrollment ids — from this member's `paths[]` or the dashboard's `overdue_paths[]`; a cancelled one is not listed there and answers 404. A path reminder carries its due date, so a finished or undated path answers `reminded: false` with the reason.",
                    "example": 5120
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reminder enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "reminded",
                        "message"
                      ],
                      "properties": {
                        "enrollment_id": {
                          "type": "integer",
                          "example": 22989,
                          "description": "For a course reminder."
                        },
                        "path_enrollment_id": {
                          "type": "integer",
                          "example": 5120,
                          "description": "For a learning-path reminder."
                        },
                        "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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`).\n`insufficient_permissions` — the token lacks `write:training` (#7183)."
          },
          "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`), or the token lacks `write:training` (`insufficient_permissions`, #7183)."
          },
          "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**Print `formatted_date` / `formatted_time`.** They are composed server-side\nin the SESSION's zone and are ready to render, which is what the web does — a\nclass happens where it happens, not where the viewer is. `starts_at` /\n`ends_at` carry the venue's UTC offset and are for sorting, countdowns and\ncalendar entries; formatting them yourself is how a learner ends up seeing\ntheir own clock instead of the class's. The pre-converted `starts_at_local` /\n`ends_at_local` were removed on 2026-09-01; the venue-local instant now lives\nin `starts_at` itself.\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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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."
                    },
                    "series": {
                      "type": "array",
                      "description": "The course's recurring classes, one entry each, to join or leave with `/training/series/{id}/membership`. Their dates are ALSO in `sessions`, as before, each with its own seat. Empty when the course has none with a date still to run.",
                      "items": {
                        "$ref": "#/components/schemas/TrainingSeriesRow"
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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\n**Required Scopes:** `read:training` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; Training's reads asked no scope\nbefore it). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:training\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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`), or the token lacks `read:training` (`insufficient_permissions`, #7183)."
          },
          "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 `destructive:training` scope** — cancelling a\nregistration is a delete-level act (#7183, #6721). 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 `destructive: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)."
          }
        }
      }
    },
    "/training/series/{id}/membership": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Join a recurring class",
        "description": "**Requires the `write:training` scope.** No request body.\n\nThe \"Recurring class\" card's **Join this class** — the native twin of the\nweb's join on the course page. One call takes a seat on every date of the\nclass still open to book (a full date puts the caller on that date's\nwaitlist), puts the class on their calendar as one series, and sends one\nnotice. The same self-booking rule as booking one seat applies:\n`can_register` on the sessions list reports it in advance.\n\n**201** when the caller joined; **200** when they were already in it\n(nothing changes — a date they gave up is not booked again). Either way the\nresponse carries the refreshed `series` entry and the sentence to show.\n\n**Refusals**, each with a stable `error.code`:\n\n- `self_enrollment_disabled` (403) — self-enrollment is off for the\n  tenant or the course, or the course must be purchased first.\n- `join_refused` (422) — the learner cannot be placed (for example a\n  prerequisite is not finished); `message` is the reason.\n- `series_over` (422) — the class has no date left to join.\n- `not_found` (404) — no such class, it has ended, or its course is not\n  visible to the caller.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The recurring class (`series[].id` on the sessions list)."
          }
        ],
        "responses": {
          "201": {
            "description": "Joined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingSeriesMembershipResponse"
                }
              }
            }
          },
          "200": {
            "description": "Already in the class; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrainingSeriesMembershipResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), the token lacks `write:training` (`insufficient_permissions`), or self-booking is closed (`self_enrollment_disabled`)."
          },
          "404": {
            "description": "No such active class in this business, or its course is not visible (`not_found`)."
          },
          "422": {
            "description": "`join_refused` or `series_over`."
          }
        }
      },
      "delete": {
        "tags": [
          "Training"
        ],
        "summary": "Leave a recurring class",
        "description": "**Requires the `destructive:training` scope** — like cancelling a seat,\nleaving gives up the caller's places on every date to come. No request body.\n\nThe card's **Leave the class**. Seats and waitlist places on the dates to\ncome are released (the head of each waitlist moves up), the class comes\noff the caller's calendar, and what they already attended stays on their\nrecord. An ended class can still be left.\n\nTo miss just one date and stay in the class (\"Can't make the next one\"),\nuse `DELETE /training/sessions/{next_session.id}/registration` instead.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left; the refreshed entry reads `member` false.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "series": {
                      "$ref": "#/components/schemas/TrainingSeriesRow"
                    },
                    "message": {
                      "type": "string",
                      "example": "You've left Weekly safety huddle."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible (`access_denied`), or the token lacks `destructive:training` (`insufficient_permissions`)."
          },
          "404": {
            "description": "No such class in this business, or its course is not visible (`not_found`)."
          },
          "422": {
            "description": "`not_a_member` — the caller is not in this class."
          }
        }
      }
    },
    "/training/check_in": {
      "post": {
        "tags": [
          "Training"
        ],
        "summary": "Check in to a class by its QR code or typed code",
        "description": "**Requires the `write:training` scope.**\n\nThe native twin of the web's class check-in: send the payload the room's\nQR code holds as `code`, or the code typed off the instructor's screen as\n`manual_code`. It runs the same check-in rule as the web and Calendar (the\ntime window, the roster, replay protection, a cap on typed attempts), and a\ncheck-in marks the learner present on the class roster. Only Training\naccess is asked — a workspace without the Calendar app checks in too.\n\n**A refusal is a 200**, not an HTTP error: `{ ok: false, status:\n\"rejected\", cause, message }`, the envelope of\n`POST /calendar/attendance/scan`. Show `message`. `cause` is stable —\n`not_invited` (not on the class's roster), `expired`, `invalid_code`,\n`rate_limited`, `qr_disabled` among others.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The scanned QR code's payload."
                  },
                  "manual_code": {
                    "type": "string",
                    "description": "The code typed off the instructor's screen (case and spaces are ignored)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checked in, already checked in, or refused (see `ok`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "recorded",
                        "walk_in",
                        "already_checked_in",
                        "rejected"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "You're checked in."
                    },
                    "cause": {
                      "type": "string",
                      "description": "Present when `ok` is false.",
                      "example": "not_invited"
                    },
                    "course_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The class's course, to open after a check-in. Null for a code that is not a class's."
                    },
                    "session_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "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`)."
          },
          "422": {
            "description": "`missing_code` — neither `code` nor `manual_code` was sent."
          }
        }
      }
    },
    "/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
                    },
                    "calendar_enabled": {
                      "type": "boolean",
                      "description": "Whether calendar pairing works here (the Calendar app is on). When false every entry's `scheduled` is false, and \"Put on calendar\" would be refused `calendar_disabled` — hide it, as the web does."
                    },
                    "can_schedule_rounding": {
                      "type": "boolean",
                      "description": "Draw \"Put on calendar\" on this: `calendar_enabled` AND the Calendar app opens for this leader (published to them, in its groups and licence) AND a credential holding write:leader_rounds, which `POST /leader-rounds/calendar-pairings` asks (a signed-in session always does). Past the Calendar door the POST is refused `calendar_not_open`."
                    },
                    "can_remove_scheduled_rounding": {
                      "type": "boolean",
                      "description": "Offer removing a row's `scheduled_pairing_id` on this: `calendar_enabled` AND destructive:leader_rounds, which `DELETE /leader-rounds/calendar-pairings/{id}` asks."
                    },
                    "can_start_round": {
                      "type": "boolean",
                      "description": "Whether this person has anyone to round on (the web's \"+ New Round\" button). False for an admin or manager with nobody in their line: hide the capture button — its person picker would be empty and every save refused.",
                      "example": true
                    },
                    "delegated_to": {
                      "type": "object",
                      "nullable": true,
                      "description": "Set while this leader's rounding is delegated: their team is on the delegate's due list (the web's \"Your rounding is with …\"). `until` is the last day, null when open-ended.",
                      "properties": {
                        "delegate_id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "until": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        }
                      }
                    },
                    "covering": {
                      "type": "array",
                      "description": "Leaders whose teams are on this person's due list for now (the web's \"You're covering …\").",
                      "items": {
                        "type": "object",
                        "properties": {
                          "principal_id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "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
                        },
                        "skipped": {
                          "type": "integer",
                          "example": 0,
                          "description": "How many of done were settled by a skip, not a round."
                        },
                        "remaining": {
                          "type": "integer",
                          "example": 4
                        },
                        "paced": {
                          "type": "boolean",
                          "example": false
                        }
                      }
                    },
                    "reminders": {
                      "type": "object",
                      "description": "The weekly-reminder card's state — the same three fields `pause-reminders` and `resume-reminders` reply with, so a freshly opened due list can show a pause and offer Resume.",
                      "required": [
                        "reminders_enabled",
                        "paused",
                        "paused_until"
                      ],
                      "properties": {
                        "reminders_enabled": {
                          "type": "boolean",
                          "description": "The TENANT-level reminder switch.",
                          "example": true
                        },
                        "paused": {
                          "type": "boolean",
                          "example": false
                        },
                        "paused_until": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the caller's snooze ends; null when not paused."
                        }
                      }
                    },
                    "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_rounded_by": {
                            "type": "object",
                            "nullable": true,
                            "description": "Who held the round behind the last-rounded date when it was not the caller — a past delegate, or the leader of a team being covered. Show \"by <name>\": the caller often cannot open that round. Null when it was the caller's own.",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "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"
                          },
                          "subject_can_open": {
                            "type": "boolean",
                            "description": "Whether this person can open Leader Rounds. False means a skip's written reason never reaches them: warn on Skip as the web and phone due lists do (\"<Name> can't open Leader Rounds, so they won't see this reason\").",
                            "example": true
                          },
                          "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 — the photo, or a generated initials image when there is none.",
                            "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. Adding a pairing that already exists answers 200 with this same id and `replayed: true`, and changes nothing.",
                            "example": 3
                          },
                          "cadence_days": {
                            "type": "integer",
                            "description": "The cadence that governs THIS person — the one set by the Rounding Program that applies to them, else the tenant default (the top-level `cadence_days`). `due` is measured against this number, so it can differ row to row.",
                            "example": 30
                          },
                          "program_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "The Rounding Program that applies to this person (it may set their template, their cadence or both); null when no program matches and the tenant default applies.",
                            "example": "New Grad Residency"
                          },
                          "template_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The template a round with this person opens on: their Rounding Program's active template, else the account default — what the web's New Round preselects. Send it as `template_id` on POST /rounds."
                          },
                          "template_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "this_month": {
                            "type": "boolean",
                            "description": "True when this person is in the current pacing window's list — up next or already done this window (see `pacing`). A client can group the due list into \"this month\" and the rest from this flag.",
                            "example": true
                          }
                        }
                      }
                    },
                    "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/widget": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "The Leader Rounds home card for the caller, in one read",
        "description": "Everything a client needs to draw its own Leader Rounds widget — built\nfor the MangoApps widget, which calls as the person through\n`POST /auth/impersonate`. `view` says which card the caller gets:\n\n- **director** — a Leader Rounds app admin or business admin (the whole\n  account), or a leader whose team includes other leaders. Carries\n  `org` (the \"My org\" tab) and `leader` (the \"My rounds\" tab).\n- **leader** — anyone else with people to round on (the same rule as the\n  Workforce dashboard widget). Carries `leader`.\n- **employee** — everyone else. Carries `subject`.\n\nEvery number is the one the page behind the card's links shows: the due\nlist and the month's pacing (`/due`), coverage over the Team Rollup's\nrolling 30-day window (`/team`), and the overdue-issue count the\nWorkforce dashboard widget prints. Each list carries at most 3 rows\nbeside its full `total`. Every `*_url` is an absolute link to the\nWorkforce web page; the `*_mobile_url` beside it is that page's phone\nversion (`/m/…`), which a phone or the Hub phone app opens instead.\n\nIssue rows (`leader.issues`, `subject.assigned`, `subject.raised`) are\nthe row shape `/leader-rounds/issues` returns, plus `completed_at`\n(when it was resolved — \"Fixed Aug 21\"; null while open), `url` and\n`mobile_url` (the issue's phone page).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's card",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "view",
                    "cadence_days",
                    "view_all_url"
                  ],
                  "properties": {
                    "view": {
                      "type": "string",
                      "enum": [
                        "director",
                        "leader",
                        "employee"
                      ]
                    },
                    "cadence_days": {
                      "type": "integer",
                      "description": "The account's default rounding cadence, in days.",
                      "example": 30
                    },
                    "view_all_url": {
                      "type": "string",
                      "description": "The Leader Rounds home page (it routes leaders and others to their own view)."
                    },
                    "view_all_mobile_url": {
                      "type": "string",
                      "description": "The phone home (`/m/apps/leader-rounds`)."
                    },
                    "leader": {
                      "type": "object",
                      "description": "Present for `leader` and `director`.",
                      "properties": {
                        "window_label": {
                          "type": "string",
                          "description": "The pacing month, e.g. \"October\"."
                        },
                        "window_end": {
                          "type": "string",
                          "format": "date"
                        },
                        "days_left": {
                          "type": "integer",
                          "description": "Whole days after today left in the month (tenant calendar).",
                          "example": 22
                        },
                        "done": {
                          "type": "integer",
                          "description": "People rounded on (or skipped) this month — \"4 of 7\".",
                          "example": 4
                        },
                        "skipped": {
                          "type": "integer",
                          "description": "How many of done were settled by a skip, not a round.",
                          "example": 0
                        },
                        "target": {
                          "type": "integer",
                          "description": "People this month calls for, by each person's cadence.",
                          "example": 7
                        },
                        "paced": {
                          "type": "boolean",
                          "description": "True when cadences are longer than a month, so `due` is this month's list rather than everyone past cadence."
                        },
                        "can_start_round": {
                          "type": "boolean",
                          "description": "Whether the caller has anyone to round on. Hide the round button when false."
                        },
                        "delegated_to": {
                          "type": "object",
                          "nullable": true,
                          "description": "Set while the caller's rounding is handed to someone else — their team is on that person's list.",
                          "properties": {
                            "delegate_id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "until": {
                              "type": "string",
                              "format": "date",
                              "nullable": true
                            }
                          }
                        },
                        "overdue_issues": {
                          "type": "integer",
                          "description": "Open, past-due issues on the rounds the caller can see — \"1 issue past due\".",
                          "example": 1
                        },
                        "assigned_open_issues": {
                          "type": "integer",
                          "description": "Open issues handed to the caller from a round they did not lead and that is not about them — \"2 issues waiting on you\".",
                          "example": 0
                        },
                        "rounds_url": {
                          "type": "string",
                          "description": "View All on the Due tab — My Rounding, the full due list."
                        },
                        "rounds_mobile_url": {
                          "type": "string",
                          "description": "Its phone page — the phone home, which shows the same due list."
                        },
                        "issues_url": {
                          "type": "string",
                          "description": "View All on the Issues tab — the Issue Ledger filtered to open issues (`?open_only=1`), the set `issues.total` counts."
                        },
                        "issues_mobile_url": {
                          "type": "string",
                          "description": "The phone Issue Ledger, filtered to open."
                        },
                        "due": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "description": "Everyone due now — \"3 Due now\", \"Round on 3\".",
                              "example": 3
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "subject_id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "job_title": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "status": {
                                    "type": "string",
                                    "enum": [
                                      "never_rounded",
                                      "overdue",
                                      "due_today",
                                      "upcoming"
                                    ],
                                    "description": "`never_rounded` and `overdue` only for someone the due list calls due. A person a recorded skip keeps quiet (a paced month list carries them) is `due_today` or `upcoming`, with `skipped: true`."
                                  },
                                  "days_overdue": {
                                    "type": "integer",
                                    "nullable": true,
                                    "description": "Days past `due_on` — \"9 days late\". Set only when `status` is overdue."
                                  },
                                  "due_on": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true,
                                    "description": "The day this person's cadence runs out — after the later of their last round and their last skip, the \"Next\" My Rounding prints. Null when never rounded and never skipped."
                                  },
                                  "last_rounded_on": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true
                                  },
                                  "last_skipped_on": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true,
                                    "description": "When a skip was last recorded for this person, else null."
                                  },
                                  "skipped": {
                                    "type": "boolean",
                                    "description": "True when a recorded skip, not a round, is why this person is not due — \"Skipped Oct 2\"."
                                  },
                                  "cadence_days": {
                                    "type": "integer"
                                  },
                                  "joined_on": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true,
                                    "description": "When they joined this business — \"Joined Aug 28\"."
                                  },
                                  "new_round_url": {
                                    "type": "string"
                                  },
                                  "new_round_mobile_url": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "issues": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "description": "Open issues on the rounds the caller can see — \"Issues (2)\"."
                            },
                            "items": {
                              "type": "array",
                              "description": "Open issues, oldest due first — the row shape `/leader-rounds/issues` returns, plus `completed_at`, `url` and `mobile_url`.",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "org": {
                      "type": "object",
                      "description": "Present for `director` only.",
                      "properties": {
                        "window_days": {
                          "type": "integer",
                          "example": 30
                        },
                        "coverage_pct": {
                          "type": "integer",
                          "example": 72
                        },
                        "coverage_expected": {
                          "type": "boolean",
                          "description": "False when nobody was due in the window — show the caption, not 0%."
                        },
                        "coverage_delegated_out": {
                          "type": "boolean"
                        },
                        "coverage_caption": {
                          "type": "string"
                        },
                        "open_issues": {
                          "type": "integer",
                          "example": 14
                        },
                        "stuck_issues": {
                          "type": "integer",
                          "description": "The Team Rollup's Red tile: issues raised in rounds these leaders led that are overdue, or closed as Won't fix.",
                          "example": 3
                        },
                        "viewer_is_app_admin": {
                          "type": "boolean"
                        },
                        "root_label": {
                          "type": "string",
                          "enum": [
                            "All Teams",
                            "My Org"
                          ],
                          "description": "\"All Teams\" for an app admin, \"My Org\" for a leader."
                        },
                        "stuck_review": {
                          "type": "object",
                          "description": "The Review N Stuck button. `count` is the Red tile (`stuck_issues`); `url` opens the Issue Ledger filtered to red (overdue, or closed as Won't fix). For an app admin the list holds exactly `count` issues; for a leader it can hold more, because it also lists red issues on rounds about them and on rounds led by someone in their line who no longer leads anyone, which `count` leaves out.",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "example": 3
                            },
                            "url": {
                              "type": "string"
                            },
                            "mobile_url": {
                              "type": "string",
                              "description": "The phone Issue Ledger, filtered to red."
                            }
                          }
                        },
                        "team_url": {
                          "type": "string"
                        },
                        "team_mobile_url": {
                          "type": "string"
                        },
                        "leaders": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "items": {
                              "type": "array",
                              "description": "Worst first. For a leader (\"My org\"), the layer below them, each unit its whole subtree. For an app admin (\"All teams\"), the Team Rollup's By Leader rows: every leader in the account, each scored on their own team.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "leader_id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "title": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "unit": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Their department, else their primary location — \"Store 112\"."
                                  },
                                  "avatar_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "rounded_subjects": {
                                    "type": "integer"
                                  },
                                  "team_size": {
                                    "type": "integer"
                                  },
                                  "coverage_pct": {
                                    "type": "integer"
                                  },
                                  "coverage_expected": {
                                    "type": "boolean"
                                  },
                                  "coverage_delegated_out": {
                                    "type": "boolean"
                                  },
                                  "coverage_caption": {
                                    "type": "string",
                                    "description": "\"2 of 8 rounded · 25%\", or why there is no score."
                                  },
                                  "band": {
                                    "type": "string",
                                    "nullable": true,
                                    "enum": [
                                      "on_track",
                                      "behind",
                                      "far_behind"
                                    ],
                                    "description": "The Team Rollup's band (80% / 50%). Null when coverage was not expected."
                                  },
                                  "band_label": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "open_issues": {
                                    "type": "integer"
                                  },
                                  "stuck_issues": {
                                    "type": "integer",
                                    "description": "The row's red count on the Team Rollup: overdue, or closed as Won't fix."
                                  },
                                  "url": {
                                    "type": "string"
                                  },
                                  "mobile_url": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "subject": {
                      "type": "object",
                      "description": "Present for `employee` only.",
                      "properties": {
                        "rounds_count": {
                          "type": "integer",
                          "description": "Rounds about the caller, skips included. 0 — \"No rounds logged with you yet\"."
                        },
                        "skipped_count": {
                          "type": "integer"
                        },
                        "last_round": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "occurred_on": {
                              "type": "string",
                              "format": "date"
                            },
                            "leader_id": {
                              "type": "integer"
                            },
                            "leader_name": {
                              "type": "string"
                            },
                            "leader_job_title": {
                              "type": "string",
                              "nullable": true
                            },
                            "leader_avatar_url": {
                              "type": "string",
                              "nullable": true
                            },
                            "issues_raised": {
                              "type": "integer"
                            },
                            "url": {
                              "type": "string"
                            },
                            "mobile_url": {
                              "type": "string",
                              "description": "The round's phone page."
                            }
                          }
                        },
                        "next_round_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "When the caller's cadence runs out — after the later of their last round and that leader's latest skip of them (or after the skip alone when they have only been skipped). Null when there is neither."
                        },
                        "overdue_issues": {
                          "type": "integer"
                        },
                        "assigned": {
                          "type": "object",
                          "description": "Open issues handed to the caller from someone else's round.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "items": {
                              "type": "array",
                              "description": "The `/leader-rounds/my` assigned row shape (with `read_only`), plus `completed_at`, `url` and `mobile_url`.",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            }
                          }
                        },
                        "raised": {
                          "type": "object",
                          "description": "Issues raised in rounds about the caller.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "open": {
                              "type": "integer"
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            }
                          }
                        },
                        "my_url": {
                          "type": "string"
                        },
                        "my_mobile_url": {
                          "type": "string",
                          "description": "The phone Rounds About Me."
                        }
                      }
                    },
                    "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`), not accessible to the caller (`app_forbidden`), or the token lacks `read:leader_rounds` (`insufficient_permissions`)."
          }
        }
      }
    },
    "/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: the one an admin\nchose as the default on the Templates page, else 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
                    },
                    "can_start_round": {
                      "type": "boolean",
                      "description": "Same answer as `/leader-rounds/due`'s: the capture form these templates draw can be saved only when true.",
                      "example": true
                    },
                    "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": "The industry pack a template belongs to — set on the pack's own templates and kept on an admin's duplicate of one; null on the industry-neutral starters and templates built from scratch.",
                            "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",
            "headers": {
              "X-Total-Count": {
                "description": "Rows in the whole list, across every page.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages at the applied `per_page`.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page this response holds (1-based).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "The page size actually applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rounds",
                    "meta"
                  ],
                  "properties": {
                    "rounds": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "substantive": {
                                "type": "boolean",
                                "description": "False for a completed round with every answer blank and no issue (the web's \"No answers\" badge — coverage does not count it, so the person stays due), and for a draft or a skip."
                              }
                            }
                          }
                        ]
                      }
                    },
                    "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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "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"
                    }
                  }
                }
              }
            }
          },
          "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 a round does NOT post a recognition. A `recognition_pick` answer\nrecords who was picked; the post to their feed is a separate step,\n`POST /leader-rounds/rounds/{id}/recognize`, which is one-shot per\nanswer. A round cannot be re-saved.\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: a\nround opens a `Capa::Action` on the stoplight ledger for every issue it\nraised, each owner notified, so one flaky upload would otherwise become\na duplicate round and phantom issues. Scoped to (business, leader) and\nenforced by a partial unique index, so two concurrent retries of one\nkey still yield one round.\n\nA replay must be the SAME capture: `subject_id` and `template_id` are\ncompared, and `occurred_on` when you send it (it defaults to today, so\na retry after midnight that omits it still replays). The key reused for\na different capture — or one already used by `POST /rounds/skip` — is\n**409 `idempotency_conflict`**: nothing is saved; send a new key.\n\nThe `Idempotency-Key` header works too, and replays the first answer\n(201, `replayed: false`, with `X-Idempotency-Cached: true`; a replay carries no `unread_notification_count`).\nEither is enough; both may be sent.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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 for the same capture returns the original round with 200 and `replayed: true` instead of creating a second one; the key reused for a different capture is 409 `idempotency_conflict`. 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 (or \"Yes\"/\"No\", \"true\"/\"false\") for yes/no; a yes/no value that names neither is refused with 422.",
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "number"
                            },
                            {
                              "type": "boolean"
                            }
                          ]
                        },
                        "referenced_user_id": {
                          "type": "integer",
                          "description": "For a `recognition_pick` question — the colleague picked."
                        },
                        "exclude_from_reporting": {
                          "type": "boolean",
                          "description": "Leave this answer out of reporting — the web and phone forms' \"Leave this answer out of themes and reports\" box. The answer stays on the round, where its leader and the person rounded on read it, and the Themes tab, AI themes and the engagement score skip it. Honoured for questions whose `excludable_from_reporting` is true (text, scale, yes/no, choice); on any other the answer saves as usual with an `exclusion_not_supported` warning."
                        }
                      }
                    },
                    "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": {
                            "substantive": {
                              "type": "boolean",
                              "description": "False for a completed round with every answer blank and no issue (the web's \"No answers\" badge — coverage does not count it, so the person stays due), and for a draft or a skip."
                            },
                            "answers_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "issues_count": {
                              "type": "integer",
                              "example": 1
                            }
                          }
                        }
                      ]
                    },
                    "replayed": {
                      "type": "boolean",
                      "description": "Always false on a 201 — this request created the round. A replay of an earlier `idempotency_key` answers 200 with `replayed: true` instead.",
                      "example": false
                    },
                    "warning_details": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsWarningDetail"
                      },
                      "description": "The same warnings as `warnings`, each with a stable `code` a client can branch on and localize, and the question it concerns. Empty when nothing was changed. A client should treat any code it does not recognise as a warning to show, using `message`."
                    },
                    "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": []
                    },
                    "warnings_known": {
                      "type": "boolean",
                      "description": "Always true on a 201. On a 200 replay it is false when the first answer's warnings are no longer kept (they are kept 24 hours) — `warnings` then holds one entry saying so (`warnings_not_replayed`), never an empty list.",
                      "example": true
                    },
                    "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 for this same capture, so the ORIGINAL round is returned and nothing new was created. `replayed` is true. Treat this as success. `warnings` repeats the first answer's warnings; if those are no longer kept, `warnings_known` is false and `warnings` holds one `warnings_not_replayed` entry instead of an empty list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round"
                  ],
                  "properties": {
                    "round": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "substantive": {
                              "type": "boolean",
                              "description": "False for a completed round with every answer blank and no issue (the web's \"No answers\" badge — coverage does not count it, so the person stays due), and for a draft or a skip."
                            },
                            "answers_count": {
                              "type": "integer",
                              "example": 5
                            },
                            "issues_count": {
                              "type": "integer",
                              "example": 1
                            }
                          }
                        }
                      ]
                    },
                    "replayed": {
                      "type": "boolean",
                      "example": true
                    },
                    "warning_details": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsWarningDetail"
                      },
                      "description": "The first answer's warnings, each with a stable `code` a client can branch on and localize, and the question it concerns. Empty only when that answer had none. When they are no longer kept, ONE entry with `code` `warnings_not_replayed` (\"This round was already recorded. Any notes about what didn't save were shown the first time.\") and `question_id` null."
                    },
                    "warnings_known": {
                      "type": "boolean",
                      "description": "False when the first answer's warnings are no longer kept, so `warnings` cannot repeat them — it says so instead.",
                      "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; `error.code` names which. `invalid_subject` — no `subject_id`, an id that is not an active member, one outside the caller's reporting subtree, or the caller themself (a self-round); all answered identically, so the refusal cannot be used to probe who exists. `invalid_template` — `template_id` is not an active template. `invalid_occurred_on` — `occurred_on` is not a readable YYYY-MM-DD date (a blank one means today). `invalid` — a required question was not answered, a list pick is no longer one of its question's choices, or the model rejected a value (a future `occurred_on`, a scale value outside 1..5, …), with the reason in `error.message`. `error.details.missing_question_ids` and `error.details.stale_choice_question_ids` (present only when non-empty) name the template questions to mark."
          },
          "409": {
            "description": "`idempotency_conflict` — the `round.idempotency_key` (or the `Idempotency-Key` header) was already used for a different capture: another `subject_id`, `template_id` or `occurred_on`, or a skip. Nothing was saved; send a new key. `idempotency_in_progress` — a request with the same `Idempotency-Key` header is still running; retry after `Retry-After` seconds to get its answer."
          }
        }
      }
    },
    "/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": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/LeaderRoundsAnswer"
                                  },
                                  {
                                    "type": "object",
                                    "required": [
                                      "recognition_postable",
                                      "recognition_refusal"
                                    ],
                                    "properties": {
                                      "recognition_postable": {
                                        "type": "boolean",
                                        "description": "Draw \"Post recognition\" on this answer ONLY when true — it is the same rule the web round page draws its button by, and the one `POST /leader-rounds/rounds/{id}/recognize` enforces. True only for a recognition pick that names someone, has not been posted yet, and that the caller could post now. False for anyone but the round's own leader (and for a leader who leads no team today), when the Recognitions app is off for the tenant, when the pick was already posted, on every other question type, and when the post would be refused — in which case `recognition_refusal` says why.",
                                        "example": true
                                      },
                                      "recognition_refusal": {
                                        "type": "object",
                                        "nullable": true,
                                        "description": "Why this recognition pick cannot be posted, known before the tap — the sentence the web prints in place of the button. Null whenever there is nothing to explain: when `recognition_postable` is true, and in every case where no button would be offered at all (not the round's leader, Recognitions off, already posted, not a recognition pick).",
                                        "required": [
                                          "code",
                                          "message"
                                        ],
                                        "properties": {
                                          "code": {
                                            "type": "string",
                                            "enum": [
                                              "not_round_leader",
                                              "not_a_recognition_answer",
                                              "no_recognition_recipient",
                                              "self_recognition",
                                              "recognition_recipient_inactive",
                                              "giving_not_allowed"
                                            ],
                                            "description": "Stable, for branching and localizing. In practice the round-detail read sends `self_recognition` (the leader picked themself), `recognition_recipient_inactive` (the person has left), `no_recognition_recipient` (the person's record no longer exists) or `giving_not_allowed` (peer recognition is off for the caller); the rest of the enum is the same refusal set the recognize endpoint answers 403/422 with.",
                                            "example": "recognition_recipient_inactive"
                                          },
                                          "message": {
                                            "type": "string",
                                            "description": "Render verbatim — the sentence the web prints.",
                                            "example": "Amy Okonkwo is no longer an active member here, so this recognition can't be posted."
                                          }
                                        },
                                        "example": null
                                      }
                                    }
                                  }
                                ]
                              }
                            },
                            "issues": {
                              "type": "array",
                              "description": "The stoplight rows this round raised, oldest first.",
                              "items": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/LeaderRoundsIssue"
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "raised_by_can_open": {
                                        "type": "boolean",
                                        "nullable": true,
                                        "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                                        "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": "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 (`[-stuck_issues, coverage]`, then name) so the\nresponse opens on the problem: `stuck_issues` is open AND overdue, while\n`red_issues` also counts closed won't-fix decisions.\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",
            "headers": {
              "X-Total-Count": {
                "description": "Rows in the whole list, across every page.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages at the applied `per_page`.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page this response holds (1-based).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "The page size actually applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "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. Summed over every leader row in scope (all of them, not the current page of `leaders`).",
                      "properties": {
                        "leaders": {
                          "type": "integer",
                          "description": "Leader rows in scope.",
                          "example": 7
                        },
                        "team_size": {
                          "type": "integer",
                          "description": "People those leaders are responsible for rounding on.",
                          "example": 58
                        },
                        "expected": {
                          "type": "integer",
                          "description": "Rounds the window expects by cadence, rounded up — what `coverage_pct` is measured against.",
                          "example": 52
                        },
                        "rounded_subjects": {
                          "type": "integer",
                          "description": "People with a SUBSTANTIVE completed round in the window.",
                          "example": 41
                        },
                        "skipped_subjects": {
                          "type": "integer",
                          "description": "People skipped in the window, within their cadence, and not rounded. A skip settles the person, so they are left out of `expected` — show the count beside the percentage.",
                          "example": 2
                        },
                        "coverage_pct": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 100,
                          "description": "`rounded_subjects` over `expected`, capped at 100. A literal 0 when nothing was expected — read `coverage_expected` and `coverage_delegated_out` before drawing it as a score.",
                          "example": 79
                        },
                        "open_issues": {
                          "type": "integer",
                          "example": 12
                        },
                        "red_issues": {
                          "type": "integer",
                          "example": 3
                        },
                        "credit_mentions": {
                          "type": "integer",
                          "description": "Credit RAISED in rounds in scope — recognition picks naming someone.",
                          "example": 9
                        },
                        "credit_posted": {
                          "type": "integer",
                          "description": "How much of `credit_mentions` actually reached Recognitions.",
                          "example": 6
                        }
                      }
                    },
                    "coverage_expected": {
                      "type": "boolean",
                      "description": "False when the window expected NO rounds anywhere in scope, in which case `summary.coverage_pct` is a literal 0 that is not a score. Render \"No rounds expected in this window\" (the web draws \"—\") rather than 0%.",
                      "example": true
                    },
                    "coverage_delegated_out": {
                      "type": "boolean",
                      "description": "TRUE when EVERY leader row in scope has handed their rounding to someone above them for this window — the same flag the unit and leader rows carry, one level up. `summary.coverage_pct` is then 0 for that reason, not because nobody rounded; render \"Rounding delegated for this window\" rather than the bar. False when there are no rows at all.",
                      "example": false
                    },
                    "credit_shown": {
                      "type": "boolean",
                      "description": "Whether to print credit (`summary.credit_mentions` / `credit_posted` and each leader row's pair). False when Recognitions is off for this company — nothing can be posted there, so the web Team Rollup hides credit.",
                      "example": true
                    },
                    "coverage_caption": {
                      "type": "string",
                      "description": "The Team Rollup's caption under the coverage tile, in whole people (\"12 of 40 rounded · not all are due this window\"). Print it beside `summary.coverage_pct` instead of `summary.expected`, which is rounded up.",
                      "example": "12 of 40 rounded"
                    },
                    "uncovered_reports_total": {
                      "type": "integer",
                      "description": "How many active members report to a manager who is no longer active here. They are counted in no row and in no coverage percentage, so print a note beside the figures (\"3 people report to a manager who is no longer active here\") — the web Team Rollup does. Only a tenant-wide app admin on the unnavigated root is told; 0 for every other view.",
                      "example": 3
                    },
                    "uncovered_reports": {
                      "type": "array",
                      "description": "The first five of those people, by first name, to name in that note; count the rest from `uncovered_reports_total`. Empty whenever the total is 0.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 5123
                          },
                          "name": {
                            "type": "string",
                            "nullable": true,
                            "example": "Aiden Fenn"
                          }
                        }
                      }
                    },
                    "viewer_is_app_admin": {
                      "type": "boolean",
                      "description": "Whether the caller is a Leader Rounds app admin (or business admin). Their root is the whole account; a leader's is their own reporting line.",
                      "example": false
                    },
                    "root_label": {
                      "type": "string",
                      "enum": [
                        "All teams",
                        "My org"
                      ],
                      "description": "What to call the scope root — the label the web uses: \"All teams\" for an app admin, \"My org\" for a leader.",
                      "example": "My org"
                    },
                    "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",
                            "nullable": true,
                            "description": "The node's leader id; null only on the `departed` row."
                          },
                          "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"
                          },
                          "expected": {
                            "type": "integer",
                            "description": "The window's obligation for this unit's whole subtree, by cadence (rounded up) — what `coverage_pct` is measured against. 0 means nothing was expected; see `coverage_delegated_out` for why that is not always \"nobody was due\".",
                            "example": 9
                          },
                          "rounded_subjects": {
                            "type": "integer"
                          },
                          "coverage_pct": {
                            "type": "number",
                            "format": "float"
                          },
                          "coverage_caption": {
                            "type": "string",
                            "description": "The Team Rollup's caption for this row's coverage, in whole people (\"1 of 5 rounded · 60% of those due\"). Where no coverage was expected it is the sentence the web prints instead: \"No rounds expected in this window\", \"Rounding delegated for this window — open issues stay theirs\", or, on the `departed` row, \"No team in the org chart — coverage not measured\". Print this rather than `expected` beside `coverage_pct`: `expected` is rounded up, so the two numbers can read as a contradiction.",
                            "example": "1 of 5 rounded · 60% of those due"
                          },
                          "open_issues": {
                            "type": "integer"
                          },
                          "red_issues": {
                            "type": "integer",
                            "description": "Overdue open issues plus closed won't-fix decisions."
                          },
                          "total_issues": {
                            "type": "integer",
                            "description": "Every issue the rounds raised, any status — the number the web row prints (\"N issues · M red\") and the ledger's \"All (N)\" one tap later."
                          },
                          "stuck_issues": {
                            "type": "integer",
                            "description": "Open and past due — the worst-first sort key."
                          },
                          "departed": {
                            "type": "boolean",
                            "description": "TRUE only on the one row an unnavigated root may carry (an app admin's: the business; a leader's: their own line): open issues from rounds led by people who no longer lead a team here, titled \"Leaders no longer in the org chart\". It has no node to drill into (`id` null, `leaf` true) and no coverage — label it, and don't read its 0% as a score. Its counts open with GET /leader-rounds/issues?former_leaders=1.",
                            "example": false
                          },
                          "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 — the photo, or a generated initials image when there is none."
                          },
                          "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"
                          },
                          "expected": {
                            "type": "integer",
                            "description": "The window's obligation for this leader by cadence (rounded up) — the number `coverage_pct` is measured against; `team_size` stays for context. 0 means no rounds were expected of them in this window.",
                            "example": 6
                          },
                          "rounded_subjects": {
                            "type": "integer"
                          },
                          "coverage_pct": {
                            "type": "number",
                            "format": "float"
                          },
                          "coverage_caption": {
                            "type": "string",
                            "description": "The Team Rollup's caption for this row's coverage, in whole people (\"1 of 5 rounded · 60% of those due\"). Where no coverage was expected it is the sentence the web prints instead: \"No rounds expected in this window\", \"Rounding delegated for this window — open issues stay theirs\", or, on the `departed` row, \"No team in the org chart — coverage not measured\". Print this rather than `expected` beside `coverage_pct`: `expected` is rounded up, so the two numbers can read as a contradiction.",
                            "example": "1 of 5 rounded · 60% of those due"
                          },
                          "open_issues": {
                            "type": "integer"
                          },
                          "red_issues": {
                            "type": "integer",
                            "description": "Overdue open issues plus closed won't-fix decisions."
                          },
                          "total_issues": {
                            "type": "integer",
                            "description": "Every issue the rounds raised, any status — the number the web row prints (\"N issues · M red\") and the ledger's \"All (N)\" one tap later."
                          },
                          "stuck_issues": {
                            "type": "integer",
                            "description": "Open and past due — the worst-first sort key."
                          },
                          "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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "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": "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": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed"
              ]
            },
            "description": "`open` — Open and In progress only (what a rollup card's \"N open\" chip counts); `closed` — resolved and won't-fix. Narrows the scope BEFORE the tally, so `tally` describes the filtered set; `color` narrows within it. Case-insensitive; off-enum values are a 400."
          },
          {
            "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": "former_leaders",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "`1` narrows to issues from rounds led by people who are not current leaders — the set the Team Rollup's `departed: true` row counts, so its numbers open the list they counted. For an app admin that is the whole business; for a leader it is the people on their own line who have left or no longer lead a team. Any other value is refused with 400 `invalid_former_leaders`, and so is `node` beside it (400 `former_leaders_with_node` — a node's leaders are all current leaders, so the pair could only ever be empty). `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",
            "headers": {
              "X-Total-Count": {
                "description": "Rows in the whole list, across every page.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages at the applied `per_page`.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page this response holds (1-based).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "The page size actually applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issues",
                    "tally",
                    "meta"
                  ],
                  "properties": {
                    "issues": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsIssue"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "raised_by_can_open": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "tally": {
                      "type": "object",
                      "description": "Stoplight counts over the whole scope — after `status` when one is sent — 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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "has_next_page": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_prev_page": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      ]
                    },
                    "viewer_is_app_admin": {
                      "type": "boolean",
                      "description": "Whether the caller is a Leader Rounds app admin (or business admin). Their root is the whole account; a leader's is their own reporting line.",
                      "example": false
                    },
                    "root_label": {
                      "type": "string",
                      "enum": [
                        "All teams",
                        "My org"
                      ],
                      "description": "What to call the scope root — the label the web uses: \"All teams\" for an app admin, \"My org\" for a leader.",
                      "example": "My org"
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`color` outside the enum (`invalid_color`), or `status` outside the enum (`invalid_status`). Both are matched case-insensitively; an off-enum value is refused rather than silently ignored."
          },
          "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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsIssue"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "raised_by_can_open": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                              "example": true
                            }
                          }
                        }
                      ]
                    },
                    "history": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsIssueHistory"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "system_generated"
                            ],
                            "properties": {
                              "system_generated": {
                                "type": "boolean",
                                "description": "True for an AUTOMATIC note — today the red-escalation note (\"Escalated automatically after N days…\"). It is stored under the round's leader because a note needs an author, but nobody wrote it: on such an entry `author` is the literal \"Automatic update\" and `author_avatar_url` is null. Draw it as a system entry with no face, as the web trail does — never as words the leader wrote.",
                                "example": false
                              },
                              "author": {
                                "type": "string",
                                "nullable": true,
                                "description": "The author's name, or the literal \"Automatic update\" when `system_generated` is true.",
                                "example": "Priya Raman"
                              }
                            }
                          }
                        }
                      ]
                    },
                    "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"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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; `\"0\"` or `null` 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. Leave the key out to leave the pillar alone. 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": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsIssue"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "raised_by_can_open": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                              "example": true
                            }
                          }
                        }
                      ]
                    },
                    "changed": {
                      "type": "boolean",
                      "example": true
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Saved, with something to know — e.g. the new owner can't open Leader Rounds, so they weren't told and can't open the issue. Show it beside the success message."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a leader (`leader_access_required`) — as on the web, only someone who leads a team today writes, so leading this round once is not enough — or app not accessible."
          },
          "404": {
            "description": "Not on the caller's manageable ledger, or no such issue."
          },
          "409": {
            "description": "Idempotency-Key header: the key was already used with a different request body (`idempotency_conflict` — send a new key), or a request with this key is still running (`idempotency_in_progress` — retry after `Retry-After` seconds to get its answer)."
          },
          "422": {
            "description": "An unusable owner, due date, priority or pillar (`invalid`), with the reasons joined in `error.message` and, field by field, in `error.details.errors_by_field` so a form can mark the failing control.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid"
                          ],
                          "example": "invalid"
                        },
                        "message": {
                          "type": "string",
                          "example": "A due date in the past would show the issue as red immediately — pick today or later."
                        },
                        "details": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "errors_by_field": {
                              "type": "object",
                              "description": "Keyed by the request field the message belongs to — `assigned_to_id`, `due_date`, `priority`, `pillar_id` — each an array of messages. Only the failing fields appear. Every field is checked, so several can fail at once, and nothing is applied when any does.",
                              "properties": {
                                "assigned_to_id": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "due_date": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "priority": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                },
                                "pillar_id": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              },
                              "example": {
                                "due_date": [
                                  "A due date in the past would show the issue as red immediately — pick today or later."
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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\n**Send an `Idempotency-Key`.** A retry after a lost response otherwise\nposts the note — and notifies the other party — a second time.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "The note. Whitespace-only is rejected. Longer text is not refused: it is cut to its first 5,000 characters, as on the web."
                  }
                }
              }
            }
          }
        },
        "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."
          },
          "409": {
            "description": "Idempotency-Key header: the key was already used with a different request body (`idempotency_conflict` — send a new key), or a request with this key is still running (`idempotency_in_progress` — retry after `Retry-After` seconds to get its answer)."
          },
          "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* **Token scope:** `write:leader_rounds`, and `destructive:leader_rounds`\n  as well for `status: cancelled` (Won't fix — a final act, #6721). A\n  token without them is 403 `insufficient_permissions`.\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. And only a LEADER writes (403 `leader_access_required`\notherwise): someone who led rounds once and leads no team now is refused,\nas on the web ledger.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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,
                    "maxLength": 4000,
                    "description": "REQUIRED when cancelling. Published to the raiser — write it for them to read, not for the audit log. Over 4,000 characters (after trimming) is refused with 422 `reason_too_long` on any status, not cut."
                  }
                }
              }
            }
          }
        },
        "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. `changed` is false when the issue already had that status: nothing was written, no audit note was added and nobody was notified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "issue",
                    "changed",
                    "history"
                  ],
                  "properties": {
                    "issue": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsIssue"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "raised_by_can_open": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                              "example": true
                            }
                          }
                        }
                      ]
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "False when the issue already had this status — nothing was written.",
                      "example": true
                    },
                    "history": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsIssueHistory"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "system_generated"
                            ],
                            "properties": {
                              "system_generated": {
                                "type": "boolean",
                                "description": "True for an AUTOMATIC note — today the red-escalation note (\"Escalated automatically after N days…\"). It is stored under the round's leader because a note needs an author, but nobody wrote it: on such an entry `author` is the literal \"Automatic update\" and `author_avatar_url` is null. Draw it as a system entry with no face, as the web trail does — never as words the leader wrote.",
                                "example": false
                              },
                              "author": {
                                "type": "string",
                                "nullable": true,
                                "description": "The author's name, or the literal \"Automatic update\" when `system_generated` is true.",
                                "example": "Priya Raman"
                              }
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unusable target status (`invalid_status`) — `status` missing or not one of the enum values. This is the ONLY 400 here; every other refusal is a 422."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a leader (`leader_access_required`) — as on the web, only someone who leads a team today writes, so leading this round once is not enough — or 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."
          },
          "409": {
            "description": "Idempotency-Key header: the key was already used with a different request body (`idempotency_conflict` — send a new key), or a request with this key is still running (`idempotency_in_progress` — retry after `Retry-After` seconds to get its answer)."
          },
          "422": {
            "description": "Cancelling without `resolution_notes` (`reason_required`); `resolution_notes` over 4,000 characters on any status (`reason_too_long` — it is refused, not cut, because it is published to the raiser); or the model refused the write (`invalid`, with the reason in `error.message`)."
          }
        }
      }
    },
    "/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  **Being the assignee grants no write**: the status belongs to the\n  leader whose ledger it is, so each row carries `read_only`, computed\n  per caller (see below) — true unless you also manage the round it\n  came from. 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 rounds your leader logged about you: completed rounds\n  AND skips (a skip's written reason is yours to read — `status` is\n  `skipped` and `skipped_reason` carries it). Drafts are not included:\n  a draft is a capture the leader has not finished, and neither are its\n  issues. Subject 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\nonly when the caller is a leader today AND leads (or supervises the\nleader of) the round it came from, in which case the honest UI points\nthem at the ledger rather than claiming 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",
            "headers": {
              "X-Total-Count": {
                "description": "Rounds about the caller, across every page — describes `meta` (the `rounds` collection) only.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages of `rounds` at the applied `per_page`.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The `rounds` page this response holds (the `page` cursor).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "The page size actually applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rounds",
                    "issues",
                    "issues_tally",
                    "assigned_issues",
                    "meta",
                    "issues_meta",
                    "assigned_issues_meta"
                  ],
                  "properties": {
                    "rounds": {
                      "type": "array",
                      "description": "Completed and skipped rounds about the caller (never drafts). Subject tier — no private notes.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "substantive": {
                                "type": "boolean",
                                "description": "False for a completed round with every answer blank and no issue (the web's \"No answers\" badge — coverage does not count it, so the person stays due), and for a draft or a skip."
                              }
                            }
                          }
                        ]
                      }
                    },
                    "issues": {
                      "type": "array",
                      "description": "Issues raised in rounds about the caller (drafts' issues excluded), open first.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsIssue"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "raised_by_can_open": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "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 from rounds they were NOT the subject of (those are already in `issues`), open first. Whether the caller may move each one is the row's own `read_only`.",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/LeaderRoundsIssue"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "raised_by_can_open": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "Whether the person who raised the issue — its round's subject — can open Leader Rounds. False means a Won't fix note never reaches them: warn as the web and phone status forms do (\"<Name> can't open Leader Rounds, so tell them another way\"). Null when no round is behind the issue.",
                                "example": true
                              }
                            }
                          },
                          {
                            "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": "True unless the caller may move this issue — a leader today who leads, or supervises the leader of, its round. Being the assignee grants nothing. A client must not render a status control when true.",
                                "example": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "has_next_page": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_prev_page": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      ]
                    },
                    "issues_meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "has_next_page": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_prev_page": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      ]
                    },
                    "assigned_issues_meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "has_next_page": {
                              "type": "boolean",
                              "example": true
                            },
                            "has_prev_page": {
                              "type": "boolean",
                              "example": false
                            }
                          }
                        }
                      ]
                    },
                    "team_conversations_waiting": {
                      "type": "array",
                      "description": "Team conversations this person is invited to and has not taken part in (the web's Take Part cards): who asked and the last day, never who else was asked or has answered. Open `take_path` in the web view to take part.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "team_listen_id": {
                            "type": "integer"
                          },
                          "asked_by": {
                            "type": "string"
                          },
                          "open_until": {
                            "type": "string",
                            "format": "date"
                          },
                          "take_path": {
                            "type": "string",
                            "example": "/apps/leader-rounds/team-listens/12/take"
                          }
                        }
                      }
                    },
                    "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 when they can open Leader Rounds\n(`LeaderRounds::Access.subject_can_open?`). 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\n**Send an idempotency key.** `idempotency_key` (top level, beside\n`subject_id`) works as `round.idempotency_key` does on\n`POST /leader-rounds/rounds`, in the same key space: a retry with the\nsame key for the same person returns the ORIGINAL skip with **200** and\n`replayed: true`; the key already used for a round, or for a skip of\nsomeone else, is **409 `idempotency_conflict`** and nothing is saved.\nThe `Idempotency-Key` header works too, and replays the first answer\nverbatim.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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 when they can open Leader Rounds.",
                    "example": "On leave until September."
                  },
                  "idempotency_key": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 255,
                    "description": "Client-generated, unique per skip attempt. A retry with the same key returns the original skip with 200 and `replayed: true` instead of filing a second one.",
                    "example": "5d0c9a7e-2f41-4b8e-9a51-0c7f3e6b2d18"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Skip recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round",
                    "replayed"
                  ],
                  "properties": {
                    "round": {
                      "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                    },
                    "replayed": {
                      "type": "boolean",
                      "description": "Always false on a 201 — this request recorded the skip.",
                      "example": false
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "200": {
            "description": "Idempotent replay — the `idempotency_key` was already used for this skip of this person, so the ORIGINAL skip is returned with `replayed: true` and nothing new was recorded. Treat as success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "round",
                    "replayed"
                  ],
                  "properties": {
                    "round": {
                      "$ref": "#/components/schemas/LeaderRoundsRoundSummary"
                    },
                    "replayed": {
                      "type": "boolean",
                      "example": true
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App not accessible, or the subject is outside the caller's subtree (`invalid_subject`)."
          },
          "409": {
            "description": "`idempotency_conflict` — the `idempotency_key` (or the `Idempotency-Key` header) was already used for a round, or for a skip of someone else. Nothing was saved; send a new key. `idempotency_in_progress` — a request with the same `Idempotency-Key` header is still running; retry after `Retry-After` seconds."
          },
          "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` says where the post landed, in the words\nevery give door uses: `active` (published), `posting` (held for the\nautomatic content check — not live yet), `pending_review` (a moderator\ndecides) or `pending_approval` (the recipient's manager approves).\nIt is null on an `already_posted` reply. A second request for the\nsame answer while one is posting is 409 `recognition_in_progress` —\nretry it to get the result; it never posts twice.\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"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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,
                      "enum": [
                        "active",
                        "posting",
                        "pending_review",
                        "pending_approval",
                        null
                      ],
                      "description": "Where the post landed: `active` (published), `posting` (held for the automatic content check — not live yet), `pending_review` (a moderator decides), `pending_approval` (the recipient's manager approves). Null when `already_posted` is true.",
                      "example": "active"
                    },
                    "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": []
                    },
                    "warning_details": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LeaderRoundsWarningDetail"
                      },
                      "description": "The same warnings as `warnings`, each with a stable `code` a client can branch on and localize, and the question it concerns. Empty when nothing went wrong around the give."
                    },
                    "answer": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsAnswer"
                        },
                        {
                          "type": "object",
                          "description": "The answer as it stands after the post. It carries the same `recognition_postable` / `recognition_refusal` pair as the round-detail read, but here they are always `false` / null: the pick has just been posted (or was already), so there is nothing left to offer — read `recognition_posted` and `recognition_status`.",
                          "properties": {
                            "recognition_postable": {
                              "type": "boolean",
                              "description": "Always false on this response.",
                              "example": false
                            },
                            "recognition_refusal": {
                              "type": "object",
                              "nullable": true,
                              "description": "Always null on this response. Same shape as on `GET /leader-rounds/rounds/{id}` ({code, message}).",
                              "properties": {
                                "code": {
                                  "type": "string"
                                },
                                "message": {
                                  "type": "string"
                                }
                              },
                              "example": null
                            }
                          }
                        }
                      ]
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a leader today (`leader_access_required` — the web gate; leading this round once is not enough), not the round's leader (`not_round_leader`), the Recognitions app this posts into is off for the organization (`recognitions_disabled` — the round detail then reports `recognition_postable: false` on every answer), or app not accessible."
          },
          "404": {
            "description": "Round not found or outside the caller's read tier (identical bodies)."
          },
          "409": {
            "description": "`recognition_in_progress` — another request is posting this same answer right now; retry to get its result (it is never posted twice). Or, for the `Idempotency-Key` header, `idempotency_conflict` (the key was used with a different body — send a new key) / `idempotency_in_progress` (retry after `Retry-After` seconds)."
          },
          "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`), the leader picked themself (`self_recognition` — a recognition has to go to someone else), the person named is no longer an active member of the organization (`recognition_recipient_inactive`), peer recognition is not enabled for the caller (`giving_not_allowed`), or the Recognitions app refused the post (`recognition_failed`, with its reason in `error.message`). Every refusal except `answer_not_found` and `recognition_failed` is known before the tap: `GET /leader-rounds/rounds/{id}` reports it per answer as `recognition_refusal`, so a client that draws the button only on `recognition_postable` should not meet them."
          }
        }
      }
    },
    "/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",
                    "meta"
                  ],
                  "properties": {
                    "subjects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 1752
                          },
                          "name": {
                            "type": "string",
                            "example": "Marcus Bell"
                          },
                          "email": {
                            "type": "string",
                            "nullable": true,
                            "description": "Null for a member with no email address — most frontline members have none, so a client must not rely on it to identify or label a person.",
                            "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 — the photo, or a generated initials image when there is none.",
                            "example": "https://acme.workforce.mangoapps.com/rails/active_storage/…"
                          },
                          "template_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The template a round with this person opens on — as on /due: their Rounding Program's, else the account default."
                          },
                          "program_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "The Rounding Program that applies to this person, null when none."
                          }
                        }
                      }
                    },
                    "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. `current_page`, `has_next_page` and `has_prev_page` are the base API's standard names, sent beside the originals so a client's shared pager reads this list too: `current_page` equals `page` and `has_next_page` equals `more`. There is no `total_count` / `total_pages`, for the reason above.",
                      "required": [
                        "page",
                        "per_page",
                        "more",
                        "current_page",
                        "has_next_page",
                        "has_prev_page"
                      ],
                      "properties": {
                        "page": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 25
                        },
                        "more": {
                          "type": "boolean",
                          "example": true
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "has_next_page": {
                          "type": "boolean",
                          "description": "Same value as `more`.",
                          "example": true
                        },
                        "has_prev_page": {
                          "type": "boolean",
                          "description": "True on any page after the first.",
                          "example": false
                        }
                      }
                    },
                    "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. The same three fields are on\n`GET /leader-rounds/due` under `reminders`, so a client can show the\nstate before anyone taps.\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": "Not a leader (`leader_access_required`) — the digest is a leader's, as on the web — or 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": "Not a leader (`leader_access_required`) — the digest is a leader's, as on the web — or 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."
                  },
                  "answered_question_ids": {
                    "type": "array",
                    "description": "Optional. The template questions the client's form already holds a non-blank answer for. They are never offered to the model and never come back in `suggestions`, so a second recap fills the question that is still blank instead of re-matching one already answered. When every fillable question is listed the response is `suggestions: []` and no model call is made. Malformed input (not an array, a non-integer member, an id not on the template) is ignored, never refused; omitting it keeps the old behaviour.",
                    "items": {
                      "type": "integer"
                    },
                    "example": [
                      101
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggestions extracted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "suggestions",
                    "excluded_question_ids",
                    "skipped_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
                      ]
                    },
                    "skipped_question_ids": {
                      "type": "array",
                      "description": "The `answered_question_ids` that were held back from the model because the form already answers them — so a client can tell \"already answered\" apart from \"the recap didn't mention it\". Empty when none were sent.",
                      "items": {
                        "type": "integer"
                      },
                      "example": [
                        101
                      ]
                    },
                    "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`), nobody to round on (`nobody_to_round` — the caller's `can_start_round` is false), AI not set up on this deployment (`ai_unavailable`), 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",
                          "nullable": true,
                          "description": "The node's display name; null if the tree cannot name it, so a client falls back to its breadcrumb entry."
                        }
                      }
                    },
                    "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"
                      }
                    },
                    "viewer_is_app_admin": {
                      "type": "boolean",
                      "description": "Whether the caller is a Leader Rounds app admin (or business admin). Their root is the whole account; a leader's is their own reporting line.",
                      "example": false
                    },
                    "root_label": {
                      "type": "string",
                      "enum": [
                        "All teams",
                        "My org"
                      ],
                      "description": "What to call the scope root — the label the web uses: \"All teams\" for an app admin, \"My org\" for a leader.",
                      "example": "My org"
                    },
                    "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\n**A retry is safe.** When this person is already on the caller's\ncalendars — most often because the first 201 was lost — the reply is\n**200** with the existing pairing and `replayed: true`: nothing was\ncreated, and the existing meeting's day and time are NOT changed (to\nmove it, remove it with `DELETE /leader-rounds/calendar-pairings/{id}`\nand add it again). `invitee_ok` is null on that reply — this request\nsent no invite.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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 the leader's own time zone (their profile choice, else their location's, else the company's), 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
                    },
                    "replayed": {
                      "type": "boolean",
                      "description": "Always false on a 201 — this request created the pairing.",
                      "example": false
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "This person is already on the caller's calendars — a retry whose 201 was lost, or a second tap. The EXISTING pairing is returned with `replayed: true`; nothing was created and its day and time were not changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pairing",
                    "replayed"
                  ],
                  "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",
                      "nullable": true,
                      "description": "Always null on a replay — this request sent no invite.",
                      "example": null
                    },
                    "replayed": {
                      "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."
          },
          "409": {
            "description": "`idempotency_conflict` — the `Idempotency-Key` header was already used for a different request. Nothing was changed; send a new key. `idempotency_in_progress` — a request with the same `Idempotency-Key` header is still running; retry after `Retry-After` seconds."
          },
          "422": {
            "description": "Calendar app not enabled (`calendar_disabled`), the Calendar app on but not open to the caller — not published to them, or outside its groups (`calendar_not_open`), subject not someone the caller owes rounds on (`invalid_subject`), 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 — either `calendar_error`, or a code passed through from the Calendar app when it refused the event — chiefly `policy_violation` (the tenant's calendar settings do not allow a recurring event) and `invalid` (an event it rejected) — with a readable `error.message`; treat any other code here as \"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 one the caller owes rounds on answer IDENTICALLY — `calendar_disabled` while the Calendar app is off, `invalid_subject` otherwise, 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. When\nthe event cannot be removed the pairing is kept and the answer is 422.\nThe token asks `destructive:leader_rounds` (a final act, #6721); without\nit the answer is 403 `insufficient_permissions`.\n\nSend an `Idempotency-Key`: a retry whose 200 was lost then gets that\n200 back instead of a 404 for a removal that worked.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Retry-safety. A repeat with the same key from the same caller replays the first SUCCESS — same status and data (a replay carries no `unread_notification_count`), with `X-Idempotency-Cached: true` — and runs nothing again. The same key with a different request body is 409 `idempotency_conflict`; a repeat sent while the first is still running is a retryable 409 `idempotency_in_progress` (wait for `Retry-After`). Refusals are not stored, so a corrected retry runs. Kept for 24 hours.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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."
          },
          "422": {
            "description": "`calendar_error` — the meeting could not be removed from the calendar. The pairing is kept (it is the only link to the meeting), so a retry tries again; not stored under the `Idempotency-Key`."
          },
          "409": {
            "description": "`idempotency_conflict` — the `Idempotency-Key` header was already used for a different request. Nothing was changed; send a new key. `idempotency_in_progress` — a request with the same `Idempotency-Key` header is still running; retry after `Retry-After` seconds."
          }
        }
      }
    },
    "/leader-rounds/team-listens": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "My team conversations before rounds",
        "description": "The caller's own \"Hear from your team before your rounds\" conversations\n— anonymous AI conversations with their team — newest first. Leaders\nonly (403 `leader_access_required`), and only while the tenant has team\nconversations on (403 `team_listens_disabled`).\n\n**Nothing of what was said is shown while one is open** — only\n`took_part`, a count: watching it change after each answer would point\nto who said what. An open one also carries `floor` and `enough_people`\n(whether at least `floor` have taken part so far): closed below the\nfloor, nothing they said is shown. A closed one carries its readout, already floored:\na topic counts people only when at least `floor` did and at least\n`floor` (or nobody) did not; quotes only from people who allowed\nquoting. Below the floor there is nothing but the count (`summary`).\n\n`state` is one of `open`, `pending` (closed, the answers are being put\ntogether), `failed` (`error` says why), `too_few` or `ready`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "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": "The caller's team conversations",
            "headers": {
              "X-Total-Count": {
                "description": "Rows in the whole list, across every page.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages at the applied `per_page`.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page this response holds (1-based).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "The page size actually applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "team_listens",
                    "meta"
                  ],
                  "properties": {
                    "team_listens": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "One \"Hear from your team before your rounds\" conversation, as its leader may see it. Open: a count only, with `floor` and whether `enough_people` have taken part so far. Closed and ready: the readout, already floored — counts of people only for groups of at least `floor`, and never who said what.",
                        "required": [
                          "id",
                          "status",
                          "state",
                          "opens_on",
                          "closes_on",
                          "invited"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "open",
                              "closed"
                            ]
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "open",
                              "pending",
                              "failed",
                              "too_few",
                              "ready"
                            ]
                          },
                          "opens_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "closes_on": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "invited": {
                            "type": "integer",
                            "description": "How many people were invited."
                          },
                          "sample_data": {
                            "type": "boolean",
                            "description": "A demo conversation from Load Sample Data — its readout is not a team's real answers."
                          },
                          "took_part": {
                            "type": "integer",
                            "description": "How many have taken part — the only count of people shown while open."
                          },
                          "floor": {
                            "type": "integer",
                            "description": "The smallest group anything said is shown for."
                          },
                          "enough_people": {
                            "type": "boolean",
                            "description": "Whether at least `floor` took part — while open, so far: closed below the floor, nothing they said is shown."
                          },
                          "summary": {
                            "type": "string",
                            "description": "Below the floor: the one sentence shown, with nothing else."
                          },
                          "error": {
                            "type": "string",
                            "nullable": true,
                            "description": "When `state` is failed: why the answers couldn't be put together. Absent in every other state, and may be null on a failed one when no reason was recorded."
                          },
                          "topics": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "key": {
                                  "type": "string",
                                  "enum": [
                                    "going_well",
                                    "in_the_way",
                                    "ask_on_rounds"
                                  ]
                                },
                                "topic": {
                                  "type": "string"
                                },
                                "question": {
                                  "type": "string",
                                  "description": "The question asked",
                                  "as written.": null
                                },
                                "people": {
                                  "type": "integer"
                                },
                                "withheld_below_floor": {
                                  "type": "boolean",
                                  "description": "Fewer than the floor answered — nothing of it is shown."
                                },
                                "what_several_people_said": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  },
                                  "description": "Points each resting on at least the floor of different people."
                                },
                                "codes": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "label": {
                                        "type": "string"
                                      },
                                      "people": {
                                        "type": "integer"
                                      },
                                      "quotes": {
                                        "type": "array",
                                        "items": {
                                          "type": "string"
                                        },
                                        "description": "Only from people who allowed quoting, with details removed."
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          },
                          "new_themes": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "label": {
                                  "type": "string"
                                },
                                "summary": {
                                  "type": "string"
                                },
                                "people": {
                                  "type": "integer"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/LeaderRoundsPagination"
                        },
                        {
                          "type": "object",
                          "description": "The base API's standard pagination names, sent beside this API's original `page` / `per_page` / `total` / `total_pages` so a client's shared pager reads this list the same way as every other /api/v1 list. `current_page` equals `page` and `total_count` equals `total`.",
                          "required": [
                            "current_page",
                            "total_count",
                            "has_next_page",
                            "has_prev_page"
                          ],
                          "properties": {
                            "current_page": {
                              "type": "integer",
                              "example": 1
                            },
                            "total_count": {
                              "type": "integer",
                              "example": 42
                            },
                            "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": "App not accessible, not a leader, or team conversations are off."
          }
        }
      }
    },
    "/leader-rounds/team-listens/{id}": {
      "get": {
        "tags": [
          "Leader Rounds"
        ],
        "summary": "One of my team conversations",
        "description": "One of the caller's own team conversations, in the same shape as the\nlist. Another leader's is 404, the same as one that does not exist.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The team conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "team_listen"
                  ],
                  "properties": {
                    "team_listen": {
                      "type": "object",
                      "description": "One \"Hear from your team before your rounds\" conversation, as its leader may see it. Open: a count only, with `floor` and whether `enough_people` have taken part so far. Closed and ready: the readout, already floored — counts of people only for groups of at least `floor`, and never who said what.",
                      "required": [
                        "id",
                        "status",
                        "state",
                        "opens_on",
                        "closes_on",
                        "invited"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "closed"
                          ]
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "open",
                            "pending",
                            "failed",
                            "too_few",
                            "ready"
                          ]
                        },
                        "opens_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "closes_on": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "invited": {
                          "type": "integer",
                          "description": "How many people were invited."
                        },
                        "sample_data": {
                          "type": "boolean",
                          "description": "A demo conversation from Load Sample Data — its readout is not a team's real answers."
                        },
                        "took_part": {
                          "type": "integer",
                          "description": "How many have taken part — the only count of people shown while open."
                        },
                        "floor": {
                          "type": "integer",
                          "description": "The smallest group anything said is shown for."
                        },
                        "enough_people": {
                          "type": "boolean",
                          "description": "Whether at least `floor` took part — while open, so far: closed below the floor, nothing they said is shown."
                        },
                        "summary": {
                          "type": "string",
                          "description": "Below the floor: the one sentence shown, with nothing else."
                        },
                        "error": {
                          "type": "string",
                          "nullable": true,
                          "description": "When `state` is failed: why the answers couldn't be put together. Absent in every other state, and may be null on a failed one when no reason was recorded."
                        },
                        "topics": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string",
                                "enum": [
                                  "going_well",
                                  "in_the_way",
                                  "ask_on_rounds"
                                ]
                              },
                              "topic": {
                                "type": "string"
                              },
                              "question": {
                                "type": "string",
                                "description": "The question asked",
                                "as written.": null
                              },
                              "people": {
                                "type": "integer"
                              },
                              "withheld_below_floor": {
                                "type": "boolean",
                                "description": "Fewer than the floor answered — nothing of it is shown."
                              },
                              "what_several_people_said": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "Points each resting on at least the floor of different people."
                              },
                              "codes": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "label": {
                                      "type": "string"
                                    },
                                    "people": {
                                      "type": "integer"
                                    },
                                    "quotes": {
                                      "type": "array",
                                      "items": {
                                        "type": "string"
                                      },
                                      "description": "Only from people who allowed quoting, with details removed."
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "new_themes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "label": {
                                "type": "string"
                              },
                              "summary": {
                                "type": "string"
                              },
                              "people": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "App not accessible, not a leader, or team conversations are off."
          },
          "404": {
            "description": "Not found, or not the caller's."
          }
        }
      }
    },
    "/communications/action-outcomes/{target_kind}/{target_id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Read source-linked action outcomes",
        "description": "Requires read:communication_outcomes, Communications app access, publication\nauthority for this business-scoped communication, and the owning source\napp's result permission. Supports policy acknowledgement, course completion,\nidentified forms and identified surveys; anonymous sources are excluded.\nFrozen audience totals and minimal source observations describe action\ncompletion following communication, not causation. No answers are returned.\nResponses are private and not cacheable. Page size is 50.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Frozen audience and current source observations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "action": {
                          "type": "object",
                          "description": "Source kind, ID, title and requested version; no answers."
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "deadline_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "audience_count": {
                          "type": "integer"
                        },
                        "eligible_count": {
                          "type": "integer"
                        },
                        "departed_count": {
                          "type": "integer"
                        },
                        "completed_before_ever_count": {
                          "type": "integer"
                        },
                        "counts": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Counts by pending, overdue, completed_before, completed_after, completed_late, not_eligible, unknown or revoked."
                        },
                        "last_reconciled_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "warning": {
                          "type": "string",
                          "nullable": true
                        },
                        "communication_fingerprint": {
                          "type": "string"
                        },
                        "page": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "interpretation": {
                          "type": "string"
                        },
                        "rows": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "user_id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "active": {
                                "type": "boolean"
                              },
                              "eligible": {
                                "type": "boolean"
                              },
                              "state": {
                                "type": "string"
                              },
                              "completed_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "observed_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "source_record_type": {
                                "type": "string",
                                "nullable": true
                              },
                              "source_record_id": {
                                "type": "integer",
                                "nullable": true
                              },
                              "reason": {
                                "type": "string",
                                "nullable": true
                              },
                              "required_occurrence_id": {
                                "type": "integer",
                                "nullable": true
                              },
                              "history": {
                                "type": "array",
                                "maxItems": 5,
                                "items": {
                                  "type": "object",
                                  "description": "Prior state, observation/completion timestamps and source record ID."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope",
            "app access": null,
            "publication or source-result permission refused": null
          },
          "404": {
            "description": "Communication or configured action unavailable in this business"
          },
          "422": {
            "description": "Communication not sent",
            "or original audience/observations incomplete": null
          }
        }
      }
    },
    "/communications/experiments/{issue_id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Read a versioned subject-experiment report",
        "description": "Requires read:communication_outcomes and the same Communications publication\nand source-result authority as the web report. Returns aggregate results\nonly; frozen input rows, assignment identities and source answers are omitted.\nSmall cells are suppressed. Missing outcomes, inadequate sample size,\nfeasibility plans and reported interference remain inconclusive. Assignment\ncompares two subjects for the same newsletter; delivery is not employee reading.\nSaved analysis versions are immutable. Independent reproduction recorded\nmeans the named reviewer's calculation and attestation were recorded; it\ndoes not certify external analyst competence or a live customer pilot.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "issue_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "version",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Saved analysis version; omitted selects the latest."
          }
        ],
        "responses": {
          "200": {
            "description": "Study definition and aggregate analysis, when available",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "issue_id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "draft",
                            "submitted",
                            "approved",
                            "rejected",
                            "withdrawn",
                            "running"
                          ]
                        },
                        "question": {
                          "type": "string"
                        },
                        "assignment_unit": {
                          "type": "string",
                          "enum": [
                            "person",
                            "site",
                            "group"
                          ]
                        },
                        "purpose": {
                          "type": "string",
                          "enum": [
                            "effect",
                            "feasibility"
                          ]
                        },
                        "observation_days": {
                          "type": "integer"
                        },
                        "deadline_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "interpretation": {
                          "type": "string"
                        },
                        "analysis": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "version": {
                              "type": "integer"
                            },
                            "as_of": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "method": {
                              "type": "string"
                            },
                            "input_digest": {
                              "type": "string"
                            },
                            "independent_reproduction_recorded": {
                              "type": "boolean"
                            },
                            "results": {
                              "type": "object",
                              "description": "Suppression, fixed group denominators, conclusion and reasons, estimate/95% interval when supportable, missing-outcome bounds, exclusions and coarse delivery facts."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope",
            "app access": null,
            "publication or source-result permission refused": null
          },
          "404": {
            "description": "Study or requested analysis version unavailable in this business"
          }
        }
      }
    },
    "/communications/delegated_reviews": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "List messages waiting for your review",
        "description": "Posts and broadcasts a delegate wrote to go out under the caller's name, waiting\nfor the caller to approve or decline them, newest first — what the web Reviews\npage shows the same person. Only the person a message is sent as sees it.\nRequires read:news_feed (posts) or read:broadcasts (broadcasts); the list holds\nonly the kinds the token may read. Communications must be on for the tenant.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's review queue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DelegatedReview"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid"
          },
          "403": {
            "description": "insufficient_permissions — the token lacks the scope; app_disabled or app_unavailable — Communications is off or not open to the caller."
          }
        }
      }
    },
    "/communications/delegated_reviews/{kind}/{id}/approve": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Approve a delegate's message (send it as yourself)",
        "description": "The caller's own send, with their permissions: a post goes out or is scheduled\n(or waits for the company's approval workflow); a broadcast runs the composer's\ngates — the send time the delegate picked, the approval workflow, content\nmoderation, then send or schedule. The delegate is told. Requires write:news_feed\nfor a post, write:broadcasts for a broadcast. Pass `seen` (the list item's `seen`)\nto refuse a version the delegate changed since the caller read it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "post",
                "broadcast"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "seen": {
                    "type": "integer",
                    "description": "The list item's `seen` value (its last-changed time, epoch seconds)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "post",
                            "broadcast"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "published",
                            "scheduled",
                            "pending_approval",
                            "held_for_moderation"
                          ],
                          "description": "published — sent now; scheduled — goes out at scheduled_at; pending_approval — with the company's reviewers, who send it; held_for_moderation — a moderator sends it."
                        },
                        "scheduled_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid"
          },
          "403": {
            "description": "insufficient_permissions — the token lacks the scope; app_disabled or app_unavailable — Communications is off or not open to the caller."
          },
          "404": {
            "description": "not_found — the message is not waiting for the caller (missing, or sent as someone else)."
          },
          "409": {
            "description": "already_decided — someone already approved or declined it; changed_since_seen — the delegate changed it since `seen` (details.review holds the current version)."
          },
          "422": {
            "description": "approval_refused — it could not be sent (details.errors say why); it is still waiting for review."
          }
        }
      }
    },
    "/communications/delegated_reviews/{kind}/{id}/decline": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Decline a delegate's message",
        "description": "Not sent: it stays in the caller's drafts, and the delegate is told, with the\ncaller's note when one is given (an app that drafted it is told nothing).\nRequires write:news_feed for a post, write:broadcasts for a broadcast.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "post",
                "broadcast"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Why it was not sent; the delegate reads it (first 300 characters)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "post",
                            "broadcast"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "returned"
                          ]
                        },
                        "delegate_told": {
                          "type": "boolean"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required or token invalid"
          },
          "403": {
            "description": "insufficient_permissions — the token lacks the scope; app_disabled or app_unavailable — Communications is off or not open to the caller."
          },
          "404": {
            "description": "not_found — the message is not waiting for the caller."
          },
          "409": {
            "description": "already_decided — someone already approved or declined it."
          }
        }
      }
    },
    "/communications/delivery/{target_kind}/{target_id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Preview channel delivery and read history",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "preview": {
                          "type": "object"
                        },
                        "destinations": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "deliveries": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "destination_id": {
                                "type": "integer"
                              },
                              "target_id": {
                                "type": "string"
                              },
                              "target_name": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              },
                              "external_id": {
                                "type": "string",
                                "nullable": true
                              },
                              "sent_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "error_message": {
                                "type": "string",
                                "nullable": true
                              },
                              "delivery_data": {
                                "type": "object"
                              }
                            }
                          }
                        },
                        "whatsapp": {
                          "type": "object",
                          "nullable": true
                        },
                        "whatsapp_deliveries": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Queue a Teams or Slack delivery",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "destination_id": {
                          "type": "integer"
                        },
                        "target_id": {
                          "type": "string"
                        },
                        "target_name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "external_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "error_message": {
                          "type": "string",
                          "nullable": true
                        },
                        "delivery_data": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "destination": {
                    "type": "string",
                    "description": "Exact destination key from the preview, connection_id:channel_id."
                  }
                },
                "required": [
                  "destination"
                ]
              }
            }
          }
        }
      }
    },
    "/communications/delivery/{target_kind}/{target_id}/{id}/retry": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Retry a failed channel delivery",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "destination_id": {
                          "type": "integer"
                        },
                        "target_id": {
                          "type": "string"
                        },
                        "target_name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "external_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "error_message": {
                          "type": "string",
                          "nullable": true
                        },
                        "delivery_data": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/delivery/{target_kind}/{target_id}/{id}/reconcile": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Reconcile an uncertain channel delivery",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "destination_id": {
                          "type": "integer"
                        },
                        "target_id": {
                          "type": "string"
                        },
                        "target_name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "external_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "sent_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "error_message": {
                          "type": "string",
                          "nullable": true
                        },
                        "delivery_data": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/delivery/{target_kind}/{target_id}/whatsapp/send": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Queue eligible WhatsApp recipients",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend. WhatsApp supports broadcast targets only. Requires an administrator-reviewed template/language and central WhatsApp sender, explicit recipient consent and a current verified unique phone identity. Preview reports eligible and excluded recipients. Credentials, raw phone fingerprints and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/delivery/{target_kind}/{target_id}/whatsapp/{id}/retry": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Retry an eligible WhatsApp delivery",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend. WhatsApp supports broadcast targets only. Requires an administrator-reviewed template/language and central WhatsApp sender, explicit recipient consent and a current verified unique phone identity. Preview reports eligible and excluded recipients. Credentials, raw phone fingerprints and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/delivery/{target_kind}/{target_id}/whatsapp/{id}/reconcile": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Reconcile an uncertain WhatsApp delivery",
        "description": "Requires the Communications app, publishing authority for the business-scoped target and read:communication_deliveries for reads or write:communication_deliveries for mutations. Only configured central integrations and explicitly allowed channels can receive deliveries. Provider delivery is distinct from employee reading or completion. Timeouts with uncertain provider results require positive reconciliation before a retry; clients must not blindly resend. WhatsApp supports broadcast targets only. Requires an administrator-reviewed template/language and central WhatsApp sender, explicit recipient consent and a current verified unique phone identity. Preview reports eligible and excluded recipients. Credentials, raw phone fingerprints and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "target_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "issue",
                "feed",
                "broadcast"
              ]
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/whatsapp/preference": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Read my WhatsApp broadcast preference",
        "description": "Requires read:own_profile and current Communications app access. The caller can read only their own preference.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "available": {
                          "type": "boolean",
                          "description": "The company sends broadcasts by WhatsApp. Turning the preference on is refused while this is false."
                        },
                        "phone_verified": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        }
      },
      "patch": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Change my WhatsApp broadcast preference",
        "description": "Requires update:own_profile and current Communications app access. Enabling requires a verified phone identity and WhatsApp delivery turned on for the company. Provider STOP and global opt-out remain separate safety gates.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "available": {
                          "type": "boolean"
                        },
                        "phone_verified": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Turning it on was refused: the company does not send broadcasts by\nWhatsApp (`available` is false on GET), or the caller's mobile\nnumber is not verified (`phone_verified`). `message` says which, in\nwords to show. Turning it off is never refused.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "invalid_preference"
                        },
                        "message": {
                          "type": "string",
                          "example": "Your company doesn't send broadcasts by WhatsApp."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "enabled"
                ]
              }
            }
          }
        }
      }
    },
    "/communications/sms/{broadcast_id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Read private SMS correspondence for my announcement",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "direction": {
                            "type": "string",
                            "enum": [
                              "inbound",
                              "outbound"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "actor_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "broadcast_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "conversation_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "body": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "pages": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "broadcast_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/communications/sms/{broadcast_id}/{id}/reply": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Reply privately by SMS",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "inbound",
                            "outbound"
                          ]
                        },
                        "status": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "actor_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "broadcast_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "conversation_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "broadcast_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",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Nonblank reply text; service enforces the configured SMS bound."
                  },
                  "operation_key": {
                    "type": "string",
                    "format": "uuid"
                  }
                },
                "required": [
                  "body",
                  "operation_key"
                ]
              }
            }
          }
        }
      }
    },
    "/communications/sms/{broadcast_id}/{id}/retry": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Retry a failed SMS reply",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "inbound",
                            "outbound"
                          ]
                        },
                        "status": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "actor_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "broadcast_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "conversation_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "broadcast_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/sms/{broadcast_id}/{id}/reconcile": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Reconcile an uncertain SMS reply",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "inbound",
                            "outbound"
                          ]
                        },
                        "status": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "actor_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "broadcast_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "conversation_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "broadcast_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/communications/sms/held": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Read unmatched inbound SMS replies",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted. Business administrator only. Held content is retained for bounded manual matching; it is not exposed to unrelated authors.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "direction": {
                            "type": "string",
                            "enum": [
                              "inbound",
                              "outbound"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "actor_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "broadcast_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "conversation_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "body": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/communications/sms/held/{id}/resolve": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Match a held SMS reply",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries for mutations and current Communications access. Announcement replies are private employee-to-author correspondence: publishing access alone does not grant access to another author’s inbox. Identity, original audience, opt-out, settings, budget and provider state are rechecked before sending. Uncertain sends must be reconciled before retry. Raw phone identities, credentials and callback nonces are omitted. Business administrator only. Matching rechecks the uniquely verified employee and frozen original recipient identity; it cannot manufacture eligibility or complete an owning-source requirement.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "inbound",
                            "outbound"
                          ]
                        },
                        "status": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "actor_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "broadcast_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "conversation_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "broadcast_id": {
                    "type": "integer",
                    "minimum": 1
                  }
                },
                "required": [
                  "broadcast_id"
                ]
              }
            }
          }
        }
      }
    },
    "/communications/sms/held/{id}/dismiss": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Dismiss and clear a held SMS reply",
        "description": "Requires read:communication_deliveries for reads or write:communication_deliveries\nfor mutations and current Communications access. Announcement replies are\nprivate employee-to-author correspondence: publishing access alone does not\ngrant access to another author’s inbox. Identity, original audience, opt-out,\nsettings, budget and provider state are rechecked before sending. Uncertain\nsends must be reconciled before retry. Raw phone identities, credentials and\ncallback nonces are omitted. Business administrator only. Matching rechecks\nthe uniquely verified employee and frozen original recipient identity; it\ncannot manufacture eligibility or complete an owning-source requirement.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:communication_deliveries` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "inbound",
                            "outbound"
                          ]
                        },
                        "status": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "actor_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "broadcast_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "conversation_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "body": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Token scope, application access or action permission refused"
          },
          "404": {
            "description": "Record unavailable in this business or to this participant"
          },
          "422": {
            "description": "Invalid request; response includes a user-facing error"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        }
      }
    },
    "/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` — every message still awaiting the\n  caller's acknowledgement: must-read posts and broadcasts (\"Needs\n  Acknowledgement\"; since 2026-10-10 it counts broadcasts too — the\n  caller's owed broadcasts that published a feed post,\n  `NewsFeed::Feed.owed_message_by`). `must_read` below stays\n  must-read posts only.\n* `counts.verified_answers` — question posts that carry a verified\n  answer (\"Verified answers\").\n* `counts.unread_messages` — the caller's unread Messages, the same\n  total as `GET /messaging/threads/unread_count`; `null` when the\n  company has Messages off or the token lacks `read:messages`.\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\n**Required Scopes:** `read:broadcasts` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\") — the Communications app's REST\nvocabulary, and the scope Ask AI asks for the same audiences. A token\nwithout it gets `403 insufficient_permissions` (\"Required scope:\nread:broadcasts\") before anything else is checked. The sign-in tokens\nhold it; a browser session is asked no scope.\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
                            },
                            "unread_messages": {
                              "type": "integer",
                              "nullable": true,
                              "example": 1,
                              "description": "Unread Messages; null when Messages is off for the company or the token lacks read:messages."
                            }
                          }
                        },
                        "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)."
                              },
                              "kind": {
                                "type": "string",
                                "enum": [
                                  "post",
                                  "announcement",
                                  "must_read",
                                  "break_through"
                                ],
                                "description": "The post's kind as Compose names it — the feed payload's `kind`; `break_through` is a Broadcast's post."
                              },
                              "kind_label": {
                                "type": "string",
                                "nullable": true,
                                "description": "The reader's word for `kind` (the company's own label when it chose one) — the feed payload's `kind_label`."
                              },
                              "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. When the post stops asking, not when it is due — draw `due_text` for the deadline."
                            },
                            "due_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "The caller's own acknowledge-by moment (the post's due date, moved on for someone asked late), or null when it asks for none."
                            },
                            "due_text": {
                              "type": "string",
                              "nullable": true,
                              "description": "The deadline in the words the web strip says (\"Due Fri, Oct 30 at 5:00 PM\", \"Was due …\"), or null."
                            },
                            "overdue": {
                              "type": "boolean",
                              "description": "Whether the caller's acknowledgement is past its due date while the post still asks."
                            },
                            "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."
                            },
                            "names_hidden": {
                              "type": "object",
                              "description": "Present only while the post's works-council group (General\nSettings › Countries) is under the company's smallest reportable\ngroup: `acknowledgement_count` and `acknowledgement_pct` then\ncount only the named people, and the group is not counted — `count`\nand `group_count` are null and `fewer_than` is that group, \"a few\"\n(the shape of `BroadcastNamesHidden`).\n",
                              "properties": {
                                "count": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "group_count": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                                },
                                "fewer_than": {
                                  "type": "integer",
                                  "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                                },
                                "reason": {
                                  "type": "string",
                                  "example": "Names are hidden for people in Germany (works council)."
                                }
                              }
                            },
                            "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 token lacks `read:broadcasts` (`insufficient_permissions`), or the Communications app is not enabled for the business or not accessible to the caller"
          }
        }
      }
    },
    "/comms_hub/publications": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "List newsletter publications",
        "description": "The business's **active** newsletter publications (`status: active`),\nordered by name, paginated. Paused and archived publications are not\nlisted (they can still be read by id).\n\nAnswers `403` when an admin turned the Communications app's\nnewsletters module off (Settings › \"Enable Newsletters\",\n`enable_publications`; on by default) — the same rule the web\nPublications page applies. The same applies to every publication and\nissue endpoint.\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/CommsHubPerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of active publications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommsHubPublication"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/CommsHubPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubPublicationsForbidden"
          }
        }
      }
    },
    "/comms_hub/publications/{id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Get a newsletter publication",
        "description": "One publication of the business, in any status (`active`, `paused`\nor `archived`).\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Publication id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The publication",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CommsHubPublication"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubPublicationsForbidden"
          },
          "404": {
            "description": "`not_found` \"Publication not found\" — no publication with this id in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/publications/{publication_id}/issues": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "List a publication's issues",
        "description": "The issues (newsletter editions) of one publication, newest first\n(`created_at` descending), paginated.\n\n**Which issues are listed.** Issues still in the authoring pipeline\n(`draft`, `in_review`, `scheduled`, `rejected`) are listed only to\ntheir author, the publication's owner, and people who may publish\nCommunications (admins, Communications app admins and the configured\npublisher group). Everyone else sees only issues that shipped\n(`sent`, `archived`) plus the issues they wrote themselves — the same\nrule the web issue page applies.\n\n**Language.** A sent issue is returned in the caller's language when a\nreviewed translation exists: `lang` if given, else the language the\ncaller was sent the issue in, else the caller's preferred language.\n`title` and `preview_text` are then the translated text, and\n`language`, `available_languages` and `source_fingerprint` are present.\nAn issue that has not been sent carries none of those three keys.\nOne sent issue (`GET …/issues/{id}`) is the reader's edition: with no\nreviewed version in that language, the whole issue translated by AI\n(`translation: ai`), or the original while that translation is made\n(`translation: pending`); `available_languages` is then every language\nit can be read in.\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "publication_id",
            "in": "path",
            "required": true,
            "description": "Publication id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/CommsHubPerPage"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only issues in this status. A value outside the enum is ignored (the filter is not applied). It never widens what the caller may see: a reader who may not see the authoring pipeline gets an empty page for `draft` unless they wrote one.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "in_review",
                "scheduled",
                "sent",
                "archived",
                "rejected"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/CommsHubIssueLang"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the issues the caller may read",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommsHubIssue"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/CommsHubPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubPublicationsForbidden"
          },
          "404": {
            "description": "`not_found` \"Publication not found\" — no publication with this id in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/publications/{publication_id}/issues/{id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Get an issue with its blocks",
        "description": "One issue of the publication, with its content blocks in order.\n\nAn issue the caller may not read (one still in the authoring pipeline\nthat they did not write, when they are not its publication's owner and\nmay not publish) answers `404 Issue not found`, exactly like a missing\none.\n\n**Blocks.** Once the issue was sent, `blocks` is the frozen copy that\nwas delivered, in the language chosen as described on the list\nendpoint: each dynamic block (`dynamic_*`) carries the resolved rows\nplus a `heading` and `description`. Before it was sent, `blocks` is\nbuilt from the live blocks — a scheduled or sent block's\n`frozen_payload`, otherwise its editable `data`.\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "publication_id",
            "in": "path",
            "required": true,
            "description": "Publication id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Issue id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/CommsHubIssueLang"
          }
        ],
        "responses": {
          "200": {
            "description": "The issue and its blocks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CommsHubIssueDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubPublicationsForbidden"
          },
          "404": {
            "description": "`not_found` — \"Publication not found\" (no such publication in the business) or \"Issue not found\" (no such issue in it, or one the caller may not read).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/audiences": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "List targeting audiences",
        "description": "The business's saved Communications audiences (targeting segments),\nordered by name, paginated, each with its cached member count.\n\nOnly people who may publish Communications (admins, Communications\napp admins and the configured publisher group) may list audiences —\nthe same gate the web Audiences page applies. Everyone else gets\n`403` \"You don't have permission to view audiences.\" The **Audiences**\nmodule is on unless an admin turned it off (`enable_audiences`).\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/CommsHubPerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of audiences",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommsHubAudience"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/CommsHubPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubAudiencesForbidden"
          }
        }
      }
    },
    "/comms_hub/audiences/{id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Get a targeting audience",
        "description": "One audience of the business with its cached member count. Same gate\nas the list: only people who may publish Communications.\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Audience id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The audience",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CommsHubAudience"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommsHubAudiencesForbidden"
          },
          "404": {
            "description": "`not_found` \"Audience not found\" — no audience with this id in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/campaigns": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "List the campaigns the caller may put a message in",
        "description": "The choices of the web's Campaign field, for a composer: the\nbusiness's campaigns that are not finished (Draft or Active) and that\nthe caller may put messages in — its owner, or any campaign for a\nCommunications admin — Active first, then Draft, then by name (at\nmost 200; a company has a handful at a time, so it is not paged).\n`can_assign` is false (and `data` empty) for someone with no campaign\nto use; draw no Campaign field for them, and a message they edit keeps\nthe campaign it is in. It (and each row's `can_change`) is false too\nfor a token without `write:broadcasts`, which every change of a\ncampaign asks.\n\nThe campaign a message is already in comes with that message's own\ndetail (`campaign` on a feed post, a broadcast or a newsletter issue).\nPut a message in a campaign with `campaign_id` on the feed and\nbroadcast create/update, or with\n`PATCH /comms_hub/campaign_membership` for any message.\n\n`403 campaigns_disabled` \"Campaigns are turned off for this\nbusiness.\" when an admin turned the Campaigns module off\n(`enable_campaigns`).\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The campaigns the caller may use",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "can_assign"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommsHubCampaign"
                      }
                    },
                    "can_assign": {
                      "type": "boolean",
                      "description": "Whether to offer a Campaign field at all."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A token without `read:broadcasts`, Communications not enabled or not open to the caller, or the Campaigns module turned off (`campaigns_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/campaigns/{id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "One campaign, its goals and the triggers it follows",
        "description": "A campaign as its web page shows it: the triggers it follows, and\neach goal's number with the breakdown it is added up from — every\nsent piece's own count, or why it is not counted. The counted pieces'\nvalues add up to the goal's `value` (for the completion rate, their\ncompletions over the people they asked). Opens are counted\ndifferently per kind of piece — a post: people who opened it in the\nfeed; a broadcast: people who viewed it; a newsletter issue: people\nwho opened it, recorded only while \"Track email opens and clicks\" is\non; a triggered message: people who opened its notification — and\nadded up across pieces, so someone who opens three pieces counts\nthree times.\n\nA trigger the campaign follows is ONE piece: every triggered message\nit sent inside the campaign's dates (`from` to `until`), however many.\n\nOpen to every member who can open Communications, as the web page is.\n`followable_triggers` lists the triggers the caller may follow from\nhere (empty unless `can_change` and the campaign is not finished).\n\n**Required Scopes:** `read:broadcasts`. A browser session is asked no\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CommsHubCampaignDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A token without `read:broadcasts`, Communications not enabled or not open to the caller, or the Campaigns module turned off (`campaigns_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Campaign not found.\" — no such campaign in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/campaigns/{id}/follows": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Follow a trigger",
        "description": "The campaign follows the trigger: every triggered message it sent\ninside the campaign's dates joins the campaign now, and each new one\njoins as it is sent, counting toward the goals with nothing attached\nby hand. A held or queued one joins when it goes out; a suppressed\nor failed one never joins; one already in another campaign stays\nthere. While the campaign is finished (Completed or Canceled) new\nones don't join; reopening it brings in those sent in its dates.\nMoving the campaign's dates re-reads them. The web page's\nFollow, with its rule: the caller must be the campaign's owner or a\nCommunications admin, and the campaign must not be finished.\n\n**Required Scopes:** `write:broadcasts`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "trigger_id"
                ],
                "properties": {
                  "trigger_id": {
                    "type": "integer",
                    "description": "A trigger from the campaign detail's `followable_triggers`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Now following",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommsHubCampaignFollowResult"
                }
              }
            }
          },
          "200": {
            "description": "It already followed that trigger (`changed` false)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommsHubCampaignFollowResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not the campaign's owner or a Communications admin; `campaigns_disabled`; or a token without `write:broadcasts`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Campaign not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`finished` — a Completed or Canceled campaign follows nothing new; `trigger_not_found` — no such trigger set up for the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/campaigns/{id}/follows/{trigger_id}": {
      "delete": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Stop following a trigger",
        "description": "The triggered messages this follow put in leave the campaign (the\nmessages themselves are kept); any attached by hand stay. Allowed on\na finished campaign too.\n\n**Required Scopes:** `write:broadcasts`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "trigger_id",
                        "removed"
                      ],
                      "properties": {
                        "trigger_id": {
                          "type": "integer"
                        },
                        "removed": {
                          "type": "integer",
                          "description": "How many triggered messages left the campaign."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not the campaign's owner or a Communications admin; `campaigns_disabled`; or a token without `write:broadcasts`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Campaign not found.\" or `not_following` — the campaign does not follow that trigger.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/campaign_membership": {
      "patch": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Put a message in a campaign, or take it out",
        "description": "Puts one message — a feed post, a broadcast or a newsletter issue —\nin the campaign `campaign_id` names, or takes it out of its campaign\n(`campaign_id: null`). The campaign's page then lists it and its\ngoals count it once it goes out. A newsletter's in-app copy is the\nsame communication as its issue: put the issue in the campaign.\n\nThe rule is the web's: the caller must be able to change the campaign\nthe message leaves and the one it joins (each one's owner or a\nCommunications admin), a campaign it joins must not be finished, and\nthe message must be theirs to place — their own, a newsletter issue\nthey manage, or one the campaign page lets them attach. A message\nthey may not place answers `404`, the same as one that does not\nexist. Their own message from before the calendar listed its kind is\ngiven its calendar row here, as saving it in Compose would.\n\n**Required Scopes:** `write:broadcasts`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:broadcasts\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_type",
                  "target_id",
                  "campaign_id"
                ],
                "properties": {
                  "target_type": {
                    "type": "string",
                    "enum": [
                      "feed",
                      "broadcast",
                      "issue"
                    ]
                  },
                  "target_id": {
                    "type": "integer",
                    "description": "The post's, broadcast's or newsletter issue's id."
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The campaign to put it in; null takes it out of its campaign."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where the message is now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "target_type",
                        "target_id",
                        "campaign",
                        "changed"
                      ],
                      "properties": {
                        "target_type": {
                          "type": "string",
                          "enum": [
                            "feed",
                            "broadcast",
                            "issue"
                          ]
                        },
                        "target_id": {
                          "type": "integer"
                        },
                        "campaign": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CommsHubCampaign"
                            }
                          ],
                          "nullable": true,
                          "description": "The campaign it is in now; null when none."
                        },
                        "changed": {
                          "type": "boolean",
                          "description": "False when it was already where it was asked to be."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — no `campaign_id` (send null to take the message out of its campaign).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` — the caller may not add messages to that campaign, or may not take the message out of the campaign it is in (the message names it); `campaigns_disabled` — the Campaigns module is off (checked before the message is looked up). Also a token without `write:broadcasts` (`insufficient_permissions`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Message not found.\" — no such message in the business, or one the caller may not place.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_target_type`, `invalid_campaign_id`, `campaign_not_found` (that campaign no longer exists), `finished` (a Completed or Canceled campaign takes no new messages) or `no_item`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/approval_requests": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "The approval requests waiting on the caller (\"My queue\")",
        "description": "The communications the caller can approve or reject now — the web\napprovals queue's \"My queue\": requests whose current step is theirs\n(their role, or people, a group or a relationship that resolves to\nthem, or any step for a Communications admin). Someone's own request\nis decided by another approver — only a company admin, who is never\nheld for approval, sees theirs — and only Communications admins see a\nbroadcast's or an alert's. Requests with a deadline come first,\nsoonest (most overdue) first, then the rest, newest first. Each row's\n`can_act` is true (false only on a token without `write:broadcasts`).\nRead one with\n`GET /comms_hub/approval_requests/{id}` before deciding it: that is\nwhere the message is, and the `content_version` the decision doors\ntake as `seen`.\n\nOpen to a Communications admin or anyone who reviews in an active\napproval workflow, while Approvals is on.\n\n**Required Scopes:** `read:broadcasts`. A token without it gets\n`403 insufficient_permissions` before anything else is checked. A\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/CommsHubPerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "The requests waiting on the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommsHubApprovalRequestRow"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/CommsHubPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`approvals_disabled` \"Approvals are disabled.\" (an admin turned Approvals off); `forbidden` \"You don't have access to the approvals queue.\" (the caller reviews in no active workflow and is not a Communications admin). Also a token without `read:broadcasts` (`insufficient_permissions`), or Communications not enabled or not open to the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/approval_requests/{id}": {
      "get": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "One approval request, with the message as its readers will see it",
        "description": "The web approval request page as data: the request — its workflow,\nevery step and who decides it, the deadline, the decisions so far\nwith who made them and why, what the caller can do with it now\n(`can_act`, `can_reject`, `can_withdraw`) — the distribution facts the\npage lists (`facts`: who\nreceives it, through which channels, when), and `communication`, the\nmessage as its readers will see it:\n\n- a **post** (`type: post`) in the fields `GET /feeds/{id}` gives\n  them: `title`, `body` (Markdown, as written — never a translation),\n  `rendered_body` (HTML, its pictures signed now), `cover_image`,\n  `media` (signed URLs, valid for an hour — read the request again\n  for fresh ones), `inline_media_ids`, `topics`, `poll` with its\n  choices, and its kind, Must-Read, acknowledgement and expiry. A\n  preview: nothing in it is reacted to, voted on or acknowledged.\n  Its pictures go to whoever may read the post and to whoever decides\n  or decided a request about it; anyone else who may open the request\n  (the rest of the reviewer queue) gets the words with\n  `pictures_shown: false`, `media: []`, no `cover_image`,\n  `inline_media_ids: []` and the pictures taken out of `body` and\n  `rendered_body`.\n- a **broadcast** (`type: broadcast`) as its reader view draws it, in\n  the fields `GET /broadcasts/{id}` gives them: `title`, `description`,\n  `hero_image`, `attachments`, its channels and timing.\n- a **newsletter** issue (`type: newsletter`): `title`,\n  `preview_text`, its publication, and `web_url` — the request's own\n  web page, which draws every block as the recipient's email will.\n- anything else (an emergency alert, a survey, a contest, a triggered\n  message): its `type`, `title` and `status`; the facts say the rest.\n\n`communication` is null when the message has been deleted.\n\nWho may open it is the web page's rule: while Approvals is on, a\nCommunications admin, anyone who reviews in an active workflow, the\nperson who asked, whoever decides it now and whoever decided it — a\nbroadcast's or an alert's request only Communications admins and its\nrequester. A decided request about a message that has not gone out\nstays with its preparers, its requester and its deciders. While\nApprovals is off, only the person who asked. The approval notice's\n\"Open request\" link (`/apps/communications/approvals/{id}`) names the same\nid.\n\nReads only. Decide it — whatever kind of message it is — at\n`POST /approvals/{id}/approve`, `/reject` and `/withdraw`, with\n`request_id` and `content_version` (as `seen`) from here.\n\n**Required Scopes:** `read:broadcasts`. A browser session is asked no\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CommsHubApprovalRequestDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden` \"You don't have access to the approvals queue.\" — the caller is none of the people above; \"You don't have access to that approval request.\" — a broadcast's or an alert's request, for someone who is neither a Communications admin nor its requester; or \"That post hasn't gone out yet — only the people preparing it can open this request.\" (the kind is named) — a decided request about a message not yet sent, for someone who neither asked nor decided. `no_longer_assigned` — the request was sent to the caller, who can no longer decide it: \"Your step was already decided — the request is with the next approver now.\", \"This request was already approved — there's nothing left for you to decide.\" (rejected, withdrawn), or, edited since, \"It was edited after it was sent for review — this is the current version.\" (the web opens the version waiting now). `approvals_disabled` \"Approvals are disabled.\" — Approvals is off and the caller did not ask. Also a token without `read:broadcasts`, or Communications not enabled or not open to the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Approval request not found.\" — no such request in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comms_hub/approval_preview": {
      "post": {
        "tags": [
          "Comms Hub"
        ],
        "summary": "Will Send hold this draft for approval, and who is asked",
        "description": "Compose's \"Goes to … for approval.\" note and the phone's \"Send for\nreview\" button, for a message still being written: whether Send will\nhold it for approval, under which workflow, and who the workflow's\nfirst step asks. The same answer desktop and phone Compose give for\nthe same draft — the send's own gate asked with the message's kind\n(a Post's `content_type` picks the workflow for questions or polls),\nits words (`title` folds in as the first line, as the send writes\nit), its #topics and topic chips, whom it reaches (a workflow limited\nby whom a message reaches is judged against it) and whose it is.\n\nA draft being sent (`feed_id` for a post, announcement or must-read;\n`broadcast_id` for a broadcast or a spot-only message) is its\nauthor's: an app admin sending a teammate's draft is told the\nteammate's workflow and approvers, as Send applies them. Only a draft\nthe caller may send is read (404 otherwise). An admin, whom no\nworkflow holds, gets `will_hold: false`. Nothing is saved.\n\nThe approvers never include the people the request would bar from\ndeciding it (its author, the sender). `note` and `send_label` are in\nthe caller's display language.\n\n**Required Scopes:** `write:broadcasts`. A browser session is asked no\nscope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "post",
                      "announcement",
                      "must_read",
                      "break_through",
                      "in_context"
                    ],
                    "default": "post",
                    "description": "The message's kind; `break_through` is the Broadcast kind."
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "update",
                      "question",
                      "poll"
                    ],
                    "description": "A Post's Kind of post (default `update`, or the draft's own)."
                  },
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string",
                    "description": "The words as written,"
                  },
                  "author_topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic chips by name, as `POST /feeds` takes them."
                  },
                  "topic_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Topic chips by id (added to `author_topics`)."
                  },
                  "rejected_topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "#topics in the words the author took off, as `POST /feeds` takes them."
                  },
                  "author_topics_listed": {
                    "type": "boolean",
                    "description": "`author_topics` is all of the draft's topics (as `PATCH /feeds/{id}` takes it)."
                  },
                  "feed_id": {
                    "type": "integer",
                    "description": "The unsent post being sent (draft or scheduled)."
                  },
                  "broadcast_id": {
                    "type": "integer",
                    "description": "The unsent broadcast being sent (draft or scheduled)."
                  },
                  "audience_type": {
                    "type": "string",
                    "enum": [
                      "everyone",
                      "segments"
                    ]
                  },
                  "audience_segment_ids": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "integer"
                        },
                        {
                          "type": "string"
                        }
                      ]
                    },
                    "description": "Saved segments (and `direct_reports`), as `POST /feeds` takes them; only those the caller is offered count."
                  },
                  "extra_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "audience_criteria": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "Attribute targeting, in the shapes `POST /feeds` documents."
                  },
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "A future time makes `send_label` \"Schedule\" when nothing holds it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "kind",
                        "will_hold",
                        "workflow",
                        "approvers",
                        "approver_count",
                        "note",
                        "send_label"
                      ],
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "will_hold": {
                          "type": "boolean",
                          "description": "Send submits it for approval instead of sending it."
                        },
                        "workflow": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "approvers": {
                          "type": "array",
                          "description": "The first 25 people the first step asks.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "approver_count": {
                          "type": "integer"
                        },
                        "note": {
                          "type": "string",
                          "nullable": true,
                          "description": "\"Goes to Ruth Okafor for approval.\" — Compose's note; null when nothing holds it."
                        },
                        "send_label": {
                          "type": "string",
                          "description": "\"Send for review\" when held, else \"Schedule\" for a future send, else \"Post\" (feed kinds) or \"Send\"."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`kind_not_available` \"You can't send this kind of message.\" — the caller may not send that kind. Also a token without `write:broadcasts` (`insufficient_permissions`), or Communications not enabled or not open to the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Draft not found.\" — `feed_id` / `broadcast_id` names no unsent draft the caller may send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_kind` — `kind` is not one of the five.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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* **`how_to_earn`** — the explanation-only twin of `earn_onramp`: the same\n  first-run caller, when the Recognition button cannot be honoured\n  (Recognitions is not reachable, or the store's recognition integration\n  is off). `headline` and `body` only — there is no link to follow. It is\n  present exactly where `earn_onramp` is absent for a first-run caller,\n  so a client draws at most one of the two.\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"
                            },
                            "refunded": {
                              "type": "integer",
                              "description": "Points cancelled or declined orders gave back (positive)."
                            },
                            "expired": {
                              "type": "integer",
                              "description": "Points the expiry policy took, as a positive magnitude."
                            },
                            "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"
                            }
                          }
                        },
                        "how_to_earn": {
                          "type": "object",
                          "description": "ABSENT unless this is a first-run caller AND `earn_onramp` is absent. Explanation only, no link.",
                          "properties": {
                            "headline": {
                              "type": "string"
                            },
                            "body": {
                              "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`within_balance=true` narrows to the items the caller can redeem\noutright with points (a points price at or under their balance), as the\nweb page's \"Within my balance\" toggle does. `within_balance_count` is\nhow many items that toggle would show under the other active filters,\nwhether or not it is on.\n\n### Two sets of counts: `filters` and `filter_counts`\n\n`filters` is the filter sheet the listing shares with\n`GET /company-store/catalog/categories`: its category rows and\ncollection chips count under region, audience, category enablement and\n`search` only — not the collection and not `within_balance`.\n\n`filter_counts` carries the same category rows and collection chips\n(same keys, same row shape) counted the way the web and phone catalogs\ncount their pills: each category row under the active `collection` and\n`within_balance` as well, each collection chip under `within_balance`\n(the chips are alternatives to one another, so the active collection is\nnot applied to them). Its \"All Categories\" row is what the grid holds.\nIt follows `featured` too, as the grid does. With none of these on, it\nequals `filters.categories` / `filters.collections`; `featured` never\nnarrows `filters`.\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` and `filter_counts`, because a count drawn from a\nwider pool advertises results 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": "within_balance",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Items the caller can redeem outright with points only — the web page's \"Within my balance\" toggle. Applied to `filter_counts`, not to `filters`."
          },
          {
            "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` and `filter_counts`, 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"
                            },
                            "filter_counts": {
                              "type": "object",
                              "description": "The category rows and collection chips of `filters`, counted under the filters in force: a category row under the active `collection` and `within_balance`, a collection chip under `within_balance`. The counts the web and phone catalog pills show. Equal to `filters.categories` / `filters.collections` when neither filter is on.",
                              "properties": {
                                "categories": {
                                  "type": "array",
                                  "items": {
                                    "$ref": "#/components/schemas/CompanyStoreCategoryRow"
                                  }
                                },
                                "collections": {
                                  "type": "array",
                                  "items": {
                                    "$ref": "#/components/schemas/CompanyStoreCollectionChip"
                                  }
                                }
                              }
                            },
                            "within_balance_count": {
                              "type": "integer",
                              "description": "How many items `within_balance=true` shows under the other active filters, whether or not it is on."
                            },
                            "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,
                        "within_balance": true,
                        "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": 9,
                          "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
                        }
                      ]
                    },
                    "filter_counts": {
                      "categories": [
                        {
                          "key": null,
                          "label": "All Categories",
                          "icon": "tag",
                          "color": "secondary",
                          "count": 13,
                          "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": 5,
                          "active": false,
                          "has_items": true
                        }
                      ],
                      "collections": [
                        {
                          "key": "travel",
                          "label": "Travel",
                          "count": 3,
                          "active": false
                        }
                      ]
                    },
                    "within_balance_count": 6,
                    "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. Like the\nlisting's `filters`, its counts do not follow the collection or \"Within\nmy balance\"; the counts under those are the listing's `filter_counts`.\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\n### `size_chart`\n\nThe size chart the web page shows under the options, as the store admin\nentered it: `{ headers: [String], rows: [[String]], note: String|null }`\n— `headers` names the columns, each row is one size (first cell the size)\npadded to the header width. `null` when the item has no chart.\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 was\nalready paid, the response reports the order alone with no `payment`\nblock rather than handing back a dead URL; if it is still owed but can\nno longer be opened (expired, closed), the answer is\n`409 checkout_session_unavailable`. The answer follows the ORDER the key\nplaced, never the `payment_type` of the retry: a `points` retry under\nthe key of a card order still awaiting its payment is answered as the\ncard checkout answers it (`placed: false` with `payment`, or the 409).\n\nA repeat answers `replayed: true` and writes nothing. When the order is\nplaced, its `message` says it was already placed (\"You already placed\nthis order (Order …)\") — show that, never a new-order celebration. When\nthe order has been cancelled since (the buyer backed out of Stripe, or\ncancelled it; an admin cancelled it; its approver turned it down), the\nanswer is `placed: true` (nothing is owed) with `replay_status`\n`cancelled` or `declined` and a `message` saying so and that a new\ncheckout orders it again — its payment page is never reopened.\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 / not approved (cancelled orders and the redemptions an approver turned down), 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* `forbidden` — `scope=all` was requested by someone who isn't a\n  store admin.\n\nA paused store (`store_enabled: false`) still serves the caller's\nown orders here — their gift-card codes and tracking are on them —\nso `store_disabled` is never returned by this endpoint.\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`;\n`processing` is `current` only while the order's status is `processing`.\nA `pending` order (placed, nobody has started on it) has no `current`\nstep: `processing` is `upcoming`, and its `detail` says what it waits for.\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), or\n`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\nDelete-level (owner decision 2026-10-04): requires the `destructive:company_store`\nscope. A personal token cannot hold it: do this in MangoApps instead.\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. Optional; whitespace-only is treated as none given. On a store ADMIN's cancel it is the reason the employee reads on their order page and in the cancellation email, exactly as on the web admin page; on the employee's own cancel it is an audit note only.",
                    "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 (a store admin\n  cancelling someone else's order on a wide token still may).\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/orders/{order_number}/request-return": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Request a return or exchange, or report a problem with a digital order",
        "description": "The buyer files a return / exchange on their OWN shipped order, or\nreports a problem with a fulfilled digital order (a gift card code that\nnever arrived or doesn't work) — the native twin of the web and phone\norder pages' forms, on the same rule (`StoreOrder#return_request_kind`).\nThe request goes to the store admins to decide. Offered as the order\ndetail's `actions.return_url` while `actions.can_return` or\n`actions.can_report_problem` is true. Refused while the store is paused.\nAsks `write:company_store`.\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": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "One of the detail's `actions.return_reasons` values — a return's physical reasons (wrong_size, defective, not_as_described, changed_mind, other), a problem report's digital ones (code_not_received, code_invalid, wrong_amount, other)."
                  },
                  "return_type": {
                    "type": "string",
                    "enum": [
                      "refund",
                      "exchange"
                    ],
                    "default": "refund",
                    "description": "Ignored for a problem report, which is always a refund."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional — anything the store admin should know."
                  },
                  "message": {
                    "type": "string",
                    "description": "Accepted in place of `notes`."
                  },
                  "requested_variants": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "An exchange's wanted variant (e.g. `{ \"Size\": \"XL\" }`); keys the item does not define are dropped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Filed; the full order detail (its `return_request` now `requested`, with `withdraw_url`), the new request, and a message for the buyer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "type": "object",
                      "description": "The order detail, as GET /company-store/orders/{order_number} renders it."
                    },
                    "return_request": {
                      "type": "object",
                      "description": "The order detail's `return_request` block for the request just filed."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not the caller's own order; `store_disabled` — the store is paused."
          },
          "404": {
            "description": "No such order in this workspace."
          },
          "409": {
            "description": "`not_returnable` — the order can't be returned or reported now (not fulfilled, not shipped, or a request is already open)."
          },
          "422": {
            "description": "`invalid_reason` — the reason isn't one this order's form takes (`details.valid_reasons` lists them); `validation_error` — the request could not be saved."
          }
        }
      }
    },
    "/company-store/orders/{order_number}/withdraw-return": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Withdraw your open return or problem report",
        "description": "The buyer takes back their open return / exchange request or gift-card\nproblem report on their OWN order — the native twin of the order page's\nWithdraw button, on the same `Store::ReturnService.withdraw!`. It closes\nas `withdrawn`, leaves every store admin's \"Needs you\" list, and frees\nthe order for a new return or report. Offered as the order detail's\n`return_request.withdraw_url`. Allowed while the store is paused.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_number",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The ORDER NUMBER (`ORD-…`)."
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn; the re-rendered order card and a message for the buyer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "type": "object",
                      "description": "The order card",
                      "as GET /orders renders a row.": null
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not the caller's own order."
          },
          "404": {
            "description": "No such order in this workspace."
          },
          "409": {
            "description": "`no_open_return` — nothing open to withdraw on this order."
          }
        }
      }
    },
    "/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, the wallet falls\nshort, or the cart passes what is left of the monthly redemption cap\nor the team store budget — both of which the points checkout refuses),\nthe one preselected (`points`, or `mixed` when points fall short and a\nsplit is offered), the split slider's ceiling (`amount.points_max`:\nthe smallest of the balance, the points that cover the card total, and\nwhat is left under the monthly cap and the team budget), the shipping\nprefill and whether Stripe collects the address for a given payment\ntype (`shipping.collected_on_stripe_for`), and the hold / budget\ndisclosures 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\nA repeat of an `idempotency_key` that already placed an order answers\nwith THAT order before any cart check, writes nothing and leaves the\ncart as it is (`replayed: true`): its still-open Stripe page\n(`placed: false` + `payment`), `409 checkout_session_unavailable` when\nthe card leg is still owed but its page can no longer be opened, or\n`placed: true` with `replay_status` and a `message` saying it was\nalready placed — or, when that order has been cancelled since (an\nabandoned card payment, the buyer, an admin) or its approver turned it\ndown, that it was, and that a new checkout orders it again. Nothing is\nowed on a cancelled order, so `placed` stays true for it: read\n`replay_status`, never `placed`, to tell the two apart.\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"
                }
              }
            }
          },
          "409": {
            "description": "`checkout_session_unavailable` — a repeat of an `idempotency_key` whose order still awaits its card payment, but whose Stripe session can no longer be opened. Nothing was written. `error.details` carries `order`, `wallet`, `complete_url` (in case it was paid) and `abandon_url` (hands the points and stock back and restores the lines to the cart) — the single-item checkout's 409."
          },
          "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, cancelled or refunded 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/order-requests/{id}/withdraw": {
      "post": {
        "tags": [
          "Company Store"
        ],
        "summary": "Withdraw your own open request",
        "description": "The sender takes back a request they sent about their order (asked by\nmistake, or no longer needed) while it is still open — the native twin\nof the order page's Withdraw button, on the same\n`Store::OrderRequestService.withdraw!`. It closes as `withdrawn`, leaves\nevery store admin's \"Needs you\" list, and a withdrawn cancellation\nrequest lets the employee ask again later. Offered as the request's\n`withdraw_url`. Allowed while the store is paused (it spends nothing).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn. 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 own requests."
          },
          "409": {
            "description": "`resolved` — the store admin already decided or answered it."
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04): requires the `destructive:company_store`\nscope.\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": "`insufficient_permissions` — the token has no `destructive:company_store` scope (see above); `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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:company_store` scope.\n",
        "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| `refund` | points a cancelled, declined or refunded order gave back | positive |\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` follows the web's pill row: All, Earned, Redemption and Refund\n  always; Adjustment and Expired only while they have rows or are\n  selected (a tenant with expiry switched off never has an expired row,\n  and a permanent \"Expired 0\" pill is noise). A client may ignore\n  `visible` and render all six — it exists so the pill row can match the\n  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 — except during a policy's notice period: when an\nadmin switches expiry on or shortens it, nothing expires for 14 days, and\n`expiring_points` counts every point the new policy reaches (the warning\nsent that night names the same figure).\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}`) plus `label`, the words\n  the web ledgers print under a row (\"Levels & Badges level-up\",\n  \"Contest prize\"). The label names the KIND of source, not the\n  record: resolving the record would mean a polymorphic load per row.\n  Fetch the underlying record by 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",
                "refund"
              ]
            }
          },
          {
            "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, Earned, Redemption, Refund, Adjustment, Expired (the web's order). 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",
                                "refund",
                                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"
                              },
                              "company_values": {
                                "type": "array",
                                "description": "Every company value it celebrates, primary first — up to three.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 4
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Customer First"
                                    }
                                  }
                                }
                              },
                              "can_share_outside": {
                                "type": "boolean",
                                "description": "Whether the caller may share this outside the company (`POST /moment_shares`) — only the person it is about, while it is public and the company's Shared moments switch is on, and only with a token holding write:recognition.",
                                "example": false
                              },
                              "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"
                              },
                              "company_values": {
                                "type": "array",
                                "description": "Every company value it celebrates, primary first — up to three.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "integer",
                                      "example": 4
                                    },
                                    "name": {
                                      "type": "string",
                                      "example": "Customer First"
                                    }
                                  }
                                }
                              },
                              "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 — the same answer as `GET /recognitions/programs`. False when peer recognition is turned off for the organization, 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
                              },
                              "nomination_block_reason": {
                                "type": "string",
                                "nullable": true,
                                "description": "Why `can_nominate` is false, in the viewer's words; null when they can nominate. Same text as the Programs list.",
                                "example": null
                              }
                            }
                          }
                        },
                        "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": "Award cycles"
                            }
                          }
                        }
                      }
                    },
                    "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 — and gives a reviewer turned down\nmore than 30 days ago, flagged `approval_state: rejected` (the last 30\ndays' are the uncounted `not_approved` group instead, never both). That\nis why `meta.total_count` on the Given tab equals `summary.total_given +\nsummary.pending_given + summary.rejected_given` and can exceed\n`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"
                          },
                          "company_values": {
                            "type": "array",
                            "description": "Every company value it celebrates, primary first — up to three.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 4
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Customer First"
                                }
                              }
                            }
                          },
                          "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",
                              "rejected",
                              null
                            ],
                            "description": "Why a give is not live yet. `posting` is the automated content check (the normal path, no human is holding it); `pending_approval` / `pending_review` need a reviewer to act; `rejected` is a give a reviewer turned down more than 30 days ago (the reason is on its permalink)."
                          },
                          "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."
                          }
                        }
                      }
                    },
                    "not_approved": {
                      "type": "array",
                      "description": "Given tab only (absent on every other tab). The caller's own gives a reviewer turned down in the last 30 days, newest decision first, one row per request (a group give names every recipient). UNCOUNTED — never in `items`, `summary` or `meta.total_count` — so a turned-down give and its reason stay findable after the notice is read. Returned whole on every page of the Given tab (it is not paginated; at most 50 requests). Once a decision is older than 30 days the give moves to `items`, flagged `approval_state: rejected`. `content` is whole and `recipients` / `company_value_ids` (primary first; `company_value_id` is the primary) are what a client's give screen prefills from to send it again.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "recipients": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "description": "The same person card `items` rows carry, plus `recognizable` — false for someone who has left or is the caller, whom a resend must not name.",
                              "properties": {
                                "recognizable": {
                                  "type": "boolean"
                                }
                              }
                            }
                          },
                          "recipient_names": {
                            "type": "string",
                            "example": "Bob Lee and Carla Diaz"
                          },
                          "content": {
                            "type": "string"
                          },
                          "company_value_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "company_value": {
                            "type": "string",
                            "nullable": true
                          },
                          "company_value_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Every value it celebrated, primary first — what a client's own give screen prefills from."
                          },
                          "company_values": {
                            "type": "array",
                            "description": "Every company value it celebrates, primary first — up to three.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 4
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Customer First"
                                }
                              }
                            }
                          },
                          "reason": {
                            "type": "string",
                            "nullable": true,
                            "description": "The reviewer's words."
                          },
                          "decided_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "decided_by": {
                            "type": "string",
                            "nullable": true,
                            "description": "Always null — automated declines record a system or fallback reviewer, so no name is given; `reason` says who decided where it matters."
                          }
                        }
                      }
                    },
                    "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
                        },
                        "rejected_given": {
                          "type": "integer",
                          "description": "Gives turned down more than 30 days ago. Present in the Given LIST (flagged `approval_state: rejected`) but never in `total_given`; the last 30 days' are `not_approved`.",
                          "example": 0
                        }
                      }
                    },
                    "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
                            },
                            "can_share_outside": {
                              "type": "boolean",
                              "description": "\"Share my year\" — whether the caller may share this recap outside the company (`POST /moment_shares`, subject_type `year_in_recognition`): only while the year holds any recognition, the company's Shared moments switch is on and the token holds write:recognition.",
                              "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. False too for a token that does not hold the full `read:recognition` grant and `destructive:recognition` — the scope `DELETE /recognitions/awards/{id}` asks.",
                              "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"
                          }
                        },
                        "birthdays": {
                          "type": "object",
                          "description": "The web page's \"Upcoming Birthdays\" card for the same window and people: only people who celebrate their birthday, and only where the caller may see date of birth. The next `display_limit` are listed, with the true total beside them. Month and day only, never a birth year or an age.",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "example": 7
                            },
                            "display_limit": {
                              "type": "integer",
                              "example": 25
                            },
                            "entries": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "user": {
                                    "type": "object",
                                    "description": "The same person card the roster rows carry."
                                  },
                                  "department": {
                                    "type": "string",
                                    "example": "Operations"
                                  },
                                  "occurs_on": {
                                    "type": "string",
                                    "format": "date",
                                    "description": "This year's (or next year's) occurrence."
                                  },
                                  "month": {
                                    "type": "integer",
                                    "example": 10
                                  },
                                  "day": {
                                    "type": "integer",
                                    "example": 4
                                  },
                                  "days_until": {
                                    "type": "integer",
                                    "example": 8
                                  }
                                }
                              }
                            }
                          }
                        },
                        "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 a viewer can\nnominate 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 + post approval requests (a group give's posts for one approver count once) 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": "`nominee_id` was not sent (error code `invalid_nominee`) — a malformed request, never answered as an unknown person. Otherwise 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\nDelete-level (owner decision 2026-10-04): requires the `destructive:recognition`\nscope.\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\nDelete-level (owner decision 2026-10-04): requires the `destructive:recognition`\nscope.\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\nDelete-level (owner decision 2026-10-04): requires the `destructive:recognition`\nscope.\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\nDelete-level (owner decision 2026-10-04): requires the `destructive:recognition`\nscope.\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/nominations/{id}/withdraw": {
      "post": {
        "tags": [
          "Recognitions"
        ],
        "summary": "Withdraw your own recognition request (nomination)",
        "description": "The nominator takes back their own award request while it still waits on a\ndecision — the native-client mirror of the web My Recognition row's\nWithdraw (Nomination#withdraw!). The reviewers' open asks are retired, the\nnomination's `status` becomes `cancelled` (its `display_status` reads\n\"Withdrawn\"), and nobody else is told. Not a reviewer action: only\nthe caller's own nomination is found, anything else is a 404.\n\nA nomination that is no longer awaiting a decision, or that belongs to an\naward cycle, is refused with 422 `not_withdrawable`.\n\n**Idempotent** — send an `Idempotency-Key` header and a retry after a\ntimeout replays the stored response instead of answering 422 for a\nwithdrawal that had in fact succeeded.\n\nDelete-level: requires the `destructive:recognition` scope, like approve and\nreject (a final act, classified by the act).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The nomination (recognition request) id, one of the caller's own in their business.",
            "schema": {
              "type": "integer"
            },
            "example": 8123
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "An opaque key that makes a retried withdraw replay the stored response instead of re-running.",
            "schema": {
              "type": "string"
            },
            "example": "withdraw-8123-6f1c2a"
          }
        ],
        "responses": {
          "200": {
            "description": "The request was withdrawn.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "nomination",
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Withdrew your nomination of Ada Lovelace."
                    },
                    "nomination": {
                      "$ref": "#/components/schemas/RecognitionNominationDecision"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`error.code` = `not_found` — no such nomination of the caller's in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "not_found"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`error.code` = `not_withdrawable` — it is no longer awaiting a decision, or it is a cycle nomination.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "not_withdrawable"
                        },
                        "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**Values.** `company_value_ids` names up to `limits.max_company_values` (3)\nof `values`, primary first; `company_value_id` (one value) is still read\nwhen the array is absent. **Points require a value:** `points > 0` needs at\nleast one — points are 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_ids": [
                      4,
                      9
                    ],
                    "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"
                }
              }
            }
          },
          "503": {
            "description": "The database was briefly unavailable — a deadlock, a lock timeout or a dropped connection (error code `unavailable`), which a retry can get past. The give almost always rolled back, but a connection lost during its final commit can leave it saved, and a failed request is not stored against its `Idempotency-Key`, so resending repeats it. Check the caller's given recognitions before resending.",
            "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_ids` | the whole list of values, primary first — up to `limits.max_company_values` (3), from `GET /recognitions/config`'s `values`. `company_value_id` is the one-value form. |\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 values alone. To CLEAR them, send\n`company_value_ids: []` (or `company_value_id` as an empty string) —\nallowed only on a recognition carrying no points (a points-bearing give\nrequires a value, the same rule the give form enforces, so clearing it\nanswers 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_ids": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "integer"
                    },
                    "description": "Every company value it celebrates, primary first, from `GET /recognitions/config` — up to three. `[]` clears them (pointless gives only).",
                    "example": [
                      12,
                      4
                    ]
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "One company value — the list's one-value form, read when `company_value_ids` is absent. Empty string clears the values (pointless gives only).",
                    "example": 12
                  }
                },
                "example": {
                  "message": "You carried the whole migration all weekend — thank you again",
                  "company_value_ids": [
                    12,
                    4
                  ]
                }
              }
            }
          }
        },
        "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_ids": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "integer"
                    }
                  },
                  "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recognition` scope.\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_ids` | the whole list of values, primary first — up to `limits.max_company_values` (3), from `GET /recognitions/config`'s `values`; `[]` clears them. `company_value_id` is the one-value form. |\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_ids": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "integer"
                    },
                    "description": "Every company value the award celebrates, primary first, from `GET /recognitions/config` — up to three. `[]` clears them.",
                    "example": [
                      12,
                      4
                    ]
                  },
                  "company_value_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "One company value — the list's one-value form, read when `company_value_ids` is absent. Empty string clears the values.",
                    "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_ids": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "integer"
                    }
                  },
                  "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recognition` scope.\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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recognition` scope.\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 and the caller is not a business admin), `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. `already_decided` — the hold is no longer waiting for approval at all (the employee withdrew it, or it was approved or declined since the queue was drawn); refresh the queue.",
            "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\nDelete-level (owner decision 2026-10-04): requires the `destructive:company_store`\nscope.\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. `already_decided` — no longer waiting for approval (withdrawn, approved or declined since the queue was drawn).",
            "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* **A place-limited Libraries admin** (a Libraries admin grant limited\n  to places or departments) sees what a reader sees, plus the libraries\n  whose audience sits inside their reach, disabled ones included.\n  `can_disable` is `true`; each card's own `can_disable` is true only\n  for a library in their reach.\n* **Everyone else** sees the 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 — and a DISABLED library only\n  when they can manage it (its creator or a named `manager`), badged.\n  `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 for Libraries admins and place-limited admins (the libraries in their reach) — 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 a library — the gate behind the row menu's Disable option and behind `POST /libraries/{id}/disable`: a Libraries admin, or a place-limited one, whose credential holds `destructive:libraries` (a session always does). App-wide, not per-library: each card's own `can_disable` answers for that library and the verb that row offers.",
                      "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 someone who can manage the library (a Libraries admin, a place-limited one in reach, its creator or a named `manager`) ever receives a `false` here.",
                                "example": true
                              },
                              "can_disable": {
                                "type": "boolean",
                                "description": "Whether this caller may take the availability verb THIS row offers: Disable on an enabled library (`POST /libraries/{id}/disable`, which asks a token `destructive:libraries`) or Enable on a disabled one (`POST /libraries/{id}/enable`, which asks `write:libraries`). False for anyone who is not a Libraries admin or a place-limited Libraries admin with this library in reach. So a token holding `write:libraries` alone sees true only on disabled rows.",
                                "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."
                              },
                              "captions": {
                                "type": "string",
                                "nullable": true,
                                "description": "The listing's captions marker for a VIDEO, the state the web and phone listings draw their \"CC\" chip from. `available` — the video has a caption track (draw \"CC\", spoken label \"Captions available\"); `coming` — a track is being generated (draw \"Captions coming\"). Null for every other item, and for a video with neither."
                              },
                              "captions_url": {
                                "type": "string",
                                "nullable": true,
                                "description": "Absolute URL of that video's WebVTT caption track — fetch it with the same bearer token (GET /libraries/{library_id}/items/{id}/captions, `text/vtt`) and show the cues while the video plays. Set exactly when `captions` is `available`; null otherwise. The web player's `<track>` route answers only a browser session, so a native client must use this URL."
                              },
                              "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
                              },
                              "can_delete": {
                                "type": "boolean",
                                "description": "May this caller delete the item from the 3-dot sheet: `can_manage` AND the token holds `destructive:libraries`, which `DELETE /libraries/{library_id}/items/{id}` asks. Draw Delete off this flag; Edit and Move stay on `can_manage` (web handoffs).",
                                "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Search everything the caller can see (the /apps/search page as data)",
        "description": "The same engine, ranking, live answers, filters and permissions as the\nweb results page (`/apps/search`), so a native app, a chat command or an\nMCP client never ranks or leaks differently from the browser. Every row\nis already scoped to the caller by the engine.\n\n### Without `q` — the page before you type\n\nAnswers `home`: what is waiting on the caller (`for_you`, sections of\nrows with a due date), their saved searches, and their recent searches\n(last 30 days, their own only). No `sections`.\n\n### With `q`\n\nA one-character `q` is refused with `400 invalid_query`, as the page\nrefuses to run it. Otherwise the body carries, in the page's order: an\noptional `live_answer` (a structured fact), the `best_match`, the ranked\n`top` rows, `shortcuts` (pages to go to), `experts` (people who can\nhelp), the `sections` by area, and `external_sections` (Enterprise\nconnected sources). `did_you_mean` suggests a spelling when the page is\nempty or thin.\n\n### Filters — the page's Filter control\n\n| Param | Web label | Values | Keeps |\n|-------|-----------|--------|-------|\n| `in` | In | a section key from `filter_options.in` | that area only (the engine runs only its leg) |\n| `since` (alias `when`) | When | `30d`, `90d`, `year` | rows dated after the cutoff; an undated row hides, except navigation sections (people, settings, apps) |\n| `mine` | Only mine | `1` (or `true`) | the caller's own rows |\n\nFilters combine. Under any filter `shortcuts` is empty and there is no\n`did_you_mean`; under `in` or `mine` there are no external sources and\nno experts. `filters` echoes what was applied (always as `since`, never\n`when`); an unknown `in` or `since` value is ignored, not an error.\n`filter_options` is the control itself — every choice with its\n`selected` state — and `active_filters` the chips, each with the params\nthat remove it.\n\n### Scope ceiling\n\nTakes `read:search` (what the MangoApps app sign-ins grant) or\n`read:my_data` (an AI connector's default grant). Either reaches every\narea except those needing their own scope:\n`password_manager` (`read:password_manager`), `my_hr_files` and\n`documents` (`read:hr_file_manager`), `compensation_management`\n(`read:compensation`), `mango_cs` (`read:mango_cs`), `source`\n(`read:source`), `field_service` (`read:field_service_suite`),\n`appointment_scheduler` (`read:appointment_scheduler`), `rfp_desk`\n(`read:rfp_response`), `lone_worker_alert` (`read:lone_worker`),\n`payroll_report` (`read:payroll`), `mango_gtm` (`read:mango_gtm`), the attendance areas\n(`timesheets`, `shifts`, `leave_requests`, `absence_reports`,\n`shift_marketplace` — `read:attendance`) and `profile_update_requests`\n(`read:employee_data`). Such a section is removed before ranking, so it\nnever reaches `best_match`, `top` or `filter_options.in` either.\n\nEach search writes one Search Analytics row with source `api`.\n",
        "operationId": "globalSearch",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The query (2–200 characters). Omit for the page before you type.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "leave policy"
          },
          {
            "name": "in",
            "in": "query",
            "required": false,
            "description": "Keep one area — a key from `filter_options.in` (e.g. `policy_hub`, `people`, `wikis`).",
            "schema": {
              "type": "string"
            },
            "example": "policy_hub"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "The \"When\" filter — keep rows dated after the cutoff.",
            "schema": {
              "type": "string",
              "enum": [
                "30d",
                "90d",
                "year"
              ]
            }
          },
          {
            "name": "when",
            "in": "query",
            "required": false,
            "description": "Alias for `since`; `since` wins when both are sent.",
            "schema": {
              "type": "string",
              "enum": [
                "30d",
                "90d",
                "year"
              ]
            }
          },
          {
            "name": "mine",
            "in": "query",
            "required": false,
            "description": "The \"Only mine\" filter — `1` or `true`.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per section in `sections` and `external_sections` (each section's `count` is still its full size).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page before you type (no `q`: `query`, `filters`, `home`) or the results (`q` given: every other property).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query",
                    "filters"
                  ],
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The query as searched — `q`, trimmed; `\"\"` for the home.",
                      "example": "leave policy"
                    },
                    "filters": {
                      "$ref": "#/components/schemas/SearchFilters"
                    },
                    "home": {
                      "type": "object",
                      "description": "Only without `q`.",
                      "properties": {
                        "for_you": {
                          "type": "array",
                          "description": "Things with a due date waiting on the caller, by area.",
                          "items": {
                            "$ref": "#/components/schemas/SearchSection"
                          }
                        },
                        "saved_searches": {
                          "type": "array",
                          "description": "The caller's saved searches, most recently used first (at most 8).",
                          "items": {
                            "$ref": "#/components/schemas/SearchSavedSearch"
                          }
                        },
                        "recent_searches": {
                          "type": "array",
                          "description": "The caller's own recent queries, newest first (at most 6, last 30 days).",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "expense form",
                            "leave policy"
                          ]
                        }
                      }
                    },
                    "active_filters": {
                      "type": "array",
                      "description": "The filter chips — each with the `filters` params that remove it.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string",
                            "example": "Last 30 days"
                          },
                          "remove": {
                            "$ref": "#/components/schemas/SearchFilters"
                          }
                        }
                      }
                    },
                    "filter_options": {
                      "type": "object",
                      "description": "The Filter control — every choice and whether it is selected.",
                      "properties": {
                        "in": {
                          "type": "array",
                          "description": "\"Everywhere\" (`key: null`) first, then the areas this search found (before filtering, so a filter never hides the way back), then the common areas this caller can open.",
                          "items": {
                            "$ref": "#/components/schemas/SearchFilterOption"
                          }
                        },
                        "since": {
                          "type": "array",
                          "description": "\"Any time\" (`key: null`), then `30d`, `90d`, `year`.",
                          "items": {
                            "$ref": "#/components/schemas/SearchFilterOption"
                          }
                        },
                        "mine": {
                          "type": "object",
                          "properties": {
                            "title": {
                              "type": "string",
                              "example": "Only mine"
                            },
                            "selected": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    },
                    "live_answer": {
                      "type": "object",
                      "nullable": true,
                      "description": "A structured fact the query asked for (e.g. \"who is my manager\"), when there is one.",
                      "additionalProperties": true
                    },
                    "best_match": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SearchRow"
                        }
                      ],
                      "nullable": true,
                      "description": "The single strongest row; null when nothing scores, or when it only repeats the live answer."
                    },
                    "top": {
                      "type": "array",
                      "description": "The ranked top rows across every area (each names its `section`).",
                      "items": {
                        "$ref": "#/components/schemas/SearchRow"
                      }
                    },
                    "shortcuts": {
                      "type": "array",
                      "description": "Pages to go to (\"Go to\"). Empty under any filter.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "experts": {
                      "type": "array",
                      "description": "People who can help — owners of the matched content, holders of a matching skill, title or place.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "reasons": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "Skill: Forklift Operation"
                            ]
                          },
                          "person": {
                            "$ref": "#/components/schemas/SearchPersonCard"
                          }
                        }
                      }
                    },
                    "sections": {
                      "type": "array",
                      "description": "The results by area, in the engine's order.",
                      "items": {
                        "$ref": "#/components/schemas/SearchSection"
                      }
                    },
                    "external_sections": {
                      "type": "array",
                      "description": "Connected external sources (Enterprise). `state` is `results`, `connect` (the caller has not signed in to the source — `connect_url` starts it) or `unavailable` (with a `reason`).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "external-7"
                          },
                          "title": {
                            "type": "string"
                          },
                          "source_id": {
                            "type": "integer"
                          },
                          "host": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "results",
                              "connect",
                              "unavailable"
                            ]
                          },
                          "reconnect": {
                            "type": "boolean"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "connect_url": {
                            "type": "string",
                            "format": "uri",
                            "description": "The person's My connections page, as an absolute URL to open in a browser, where they connect (or, with `reconnect`, reconnect) the source. Present only when `state` is `connect`."
                          },
                          "more_url": {
                            "type": "string",
                            "description": "Where to see the rest when the source has more matches than this page shows: the source's own site, usually its search for this query (Slack opens the workspace). Opens outside the product."
                          },
                          "next_url": {
                            "type": "string",
                            "description": "The next page of this source's results, as a web page path on the person's site. Open it in a browser signed in as the same person; it is bound to them, this query and their current connection, and is valid for 15 minutes (an expired one starts the results again from the first page)."
                          },
                          "date_filtered": {
                            "type": "boolean",
                            "description": "True when a `since` filter removed some of this source's rows."
                          },
                          "count": {
                            "type": "integer"
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/SearchRow"
                            }
                          }
                        }
                      }
                    },
                    "did_you_mean": {
                      "type": "string",
                      "nullable": true,
                      "description": "A suggested query when the results are empty or thin (never under a filter)."
                    },
                    "saved_search": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SearchSavedSearch"
                        }
                      ],
                      "nullable": true,
                      "description": "This exact search (query and filters) when the caller has saved it — its `id` is what DELETE /search/saved_searches/{id} takes; save it with POST /search/saved_searches."
                    },
                    "ask_ai_enabled": {
                      "type": "boolean",
                      "description": "Whether Ask AI is available to the caller (the page's \"Open Ask AI\")."
                    },
                    "ai_overview_available": {
                      "type": "boolean",
                      "description": "Whether the page would show an AI overview for this search."
                    },
                    "ai_overview_url": {
                      "type": "string",
                      "nullable": true,
                      "description": "The GET /search/overview URL to call for the AI overview card, after drawing the results (the page lazy-loads it). Present only when the page would show the card — Ask AI on for the caller, no live answer, no filter — and carries `ext`, the external sources that answered.",
                      "example": "/api/v1/search/overview?q=leave+policy"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "elapsed_ms": {
                          "type": "integer",
                          "nullable": true
                        },
                        "total": {
                          "type": "integer",
                          "description": "Rows returned, including ranked rows that belong to no section (meaning matches, promoted)."
                        },
                        "match_total": {
                          "type": "integer",
                          "description": "The page's \"N results\" — each section's full match count where its area capped the rows, plus external rows."
                        },
                        "external_total": {
                          "type": "integer"
                        },
                        "nothing_found": {
                          "type": "boolean",
                          "description": "True when the page would show \"Nothing matched\"."
                        },
                        "incomplete": {
                          "type": "boolean",
                          "description": "True when some areas did not answer (out of time, or failed) and are missing from this response; the page says \"Some areas could not be searched just now\". Search again to retry."
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "results": {
                    "summary": "GET /search?q=leave%20policy&since=90d",
                    "value": {
                      "query": "leave policy",
                      "filters": {
                        "since": "90d"
                      },
                      "active_filters": [
                        {
                          "label": "Last 90 days",
                          "remove": {}
                        }
                      ],
                      "filter_options": {
                        "in": [
                          {
                            "key": null,
                            "title": "Everywhere",
                            "selected": true
                          },
                          {
                            "key": "policy_hub",
                            "title": "Policies",
                            "selected": false
                          }
                        ],
                        "since": [
                          {
                            "key": null,
                            "title": "Any time",
                            "selected": false
                          },
                          {
                            "key": "30d",
                            "title": "Last 30 days",
                            "selected": false
                          },
                          {
                            "key": "90d",
                            "title": "Last 90 days",
                            "selected": true
                          },
                          {
                            "key": "year",
                            "title": "This year",
                            "selected": false
                          }
                        ],
                        "mine": {
                          "title": "Only mine",
                          "selected": false
                        }
                      },
                      "live_answer": null,
                      "best_match": {
                        "id": 42,
                        "title": "Leave Policy",
                        "url": "/apps/policy_hub/policies/42",
                        "section": "policy_hub",
                        "section_title": "Policies",
                        "updated_at": "2026-09-30T10:00:00Z"
                      },
                      "top": [],
                      "shortcuts": [],
                      "experts": [],
                      "sections": [
                        {
                          "key": "policy_hub",
                          "title": "Policies",
                          "count": 1,
                          "see_all_url": "/apps/policy_hub/policies?q=leave+policy",
                          "see_all_title": "Policy Hub",
                          "items": [
                            {
                              "id": 42,
                              "title": "Leave Policy",
                              "url": "/apps/policy_hub/policies/42",
                              "icon": "fa-solid fa-file-shield",
                              "updated_at": "2026-09-30T10:00:00Z"
                            }
                          ]
                        }
                      ],
                      "external_sections": [],
                      "did_you_mean": null,
                      "saved_search": null,
                      "ask_ai_enabled": true,
                      "ai_overview_available": false,
                      "meta": {
                        "elapsed_ms": 412,
                        "total": 1,
                        "match_total": 1,
                        "external_total": 0,
                        "nothing_found": false,
                        "incomplete": false
                      }
                    }
                  },
                  "home": {
                    "summary": "GET /search (no q)",
                    "value": {
                      "query": "",
                      "filters": {},
                      "home": {
                        "for_you": [],
                        "saved_searches": [
                          {
                            "id": 3,
                            "name": "expense form",
                            "query": "expense form",
                            "filters": {
                              "since": "30d"
                            },
                            "alerting": true,
                            "alert_cadence": "weekly",
                            "cadence_label": "Weekly"
                          }
                        ],
                        "recent_searches": [
                          "leave policy"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`q` is one character (`invalid_query`).",
            "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 holds neither `read:search` nor `read:my_data` (`insufficient_permissions`).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search/overview": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "The AI overview card for a search",
        "description": "The AI overview the results page lazy-loads after it has painted, as\ndata. Call the `ai_overview_url` that `GET /search` returns: it is\npresent only when the page would show the card.\n\n`status` is `answered` (the `text` with its `[n]` markers, `from`, one\nsource chip per document with its trust line, and every cited marker),\n`not_documented` (show \"Nothing in company knowledge answers this\ndirectly — the results below may.\") or `skipped` (draw nothing — Ask AI\nor company knowledge is off for the caller).\n\nRuns a model on the organization's AI credits, like the web card. An\nanswer that read an external source is never cached and is sent with\n`Cache-Control: no-store`. Takes `read:search` or `read:my_data`.\n",
        "operationId": "searchOverview",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 200
            }
          },
          {
            "name": "ext",
            "in": "query",
            "required": false,
            "description": "External source ids that answered, comma-separated — as given in `ai_overview_url`.",
            "schema": {
              "type": "string",
              "example": "3,7"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The card.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "answered",
                        "not_documented",
                        "skipped"
                      ]
                    },
                    "text": {
                      "type": "string",
                      "description": "The answer, with [n] markers naming `citations`. Answered only.",
                      "example": "Full-time employees accrue 20 days of paid time off a year [1][2]."
                    },
                    "from": {
                      "type": "string",
                      "description": "Where the answer came from, for the card's label. Answered only.",
                      "example": "company knowledge and SharePoint — Sample"
                    },
                    "sources": {
                      "type": "array",
                      "description": "One chip per cited document.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "marker": {
                            "type": "string",
                            "example": "[1]"
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "description": "Absent when the source's link is not safe to open; keep the chip as text."
                          },
                          "external": {
                            "type": "boolean",
                            "description": "Opens outside MangoApps."
                          },
                          "trust": {
                            "type": "array",
                            "description": "The chip's trust line, in order — who it is for, one trust word, the owner.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string",
                                  "example": "Review overdue"
                                },
                                "warning": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "citations": {
                      "type": "array",
                      "description": "Every cited marker, for linking the [n] markers in `text`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "marker": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "external": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "took_ms": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`q` is shorter than 2 characters (`invalid_query`).",
            "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 holds neither `read:search` nor `read:my_data` (`insufficient_permissions`).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search/recent": {
      "delete": {
        "tags": [
          "Search"
        ],
        "summary": "Clear the caller's recent searches",
        "description": "The Search page's **Clear** beside Recent searches, as data: from now on\n`home.recent_searches` lists only what the caller searches afterwards, on\nevery device, and the web header box drops what was typed before it\nafter the next full page load in each browser. The search log itself is kept (Analytics reads it in\naggregate). Only the caller's own list, in this organization. Needs\n`write:personal`.\n",
        "operationId": "searchClearRecent",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cleared.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cleared": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Recent searches cleared."
                    }
                  }
                }
              }
            }
          },
          "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 does not hold `write:personal` (`insufficient_permissions`).",
            "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": "The caller is not a member of this organization.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search/lookahead": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Lookahead — the results listed while the caller is still typing",
        "description": "What the search box shows before the caller presses Enter: the same\nfast engine, live answers, promotions, ranking and permissions as the\nweb omnibox (`/global_search?fast=1`), so a native app's lookahead\nnever ranks or leaks differently from the browser's. Call it on a\ndebounced keystroke; for the full results (every area, filters,\nexperts, connected sources) call `GET /search?q=` when the caller\npresses Enter or taps \"See all results\".\n\nIt runs only the fast areas (people, apps, settings, policies, wikis,\ndocuments, tasks, …) plus the areas of the app named in `app`, and it\nnever records a search in Search Analytics — a keystroke is not a\nsearch; `GET /search` is.\n\n### The body, in the order a client draws it\n\n* `live_answer` — a structured fact the query asked for (\"next holiday\",\n  \"my manager\"), when there is one.\n* `best_match` — the single strongest row; null when nothing scores, or\n  when it only repeats the live answer.\n* `rows` — the flat list the phone lookahead draws (at most 8): the live\n  answer and the ranked rows first, then the rest in area order, with\n  rows that would look identical folded into one.\n* `shortcuts` — pages to go to (\"Go to\").\n* `sections` — the results by area; each carries its full `count`, at\n  most `limit` `items`, and `see_all_url` (\"View all policies\") where\n  the area has its own search.\n\nRows carry their `actions` (Acknowledge a pending policy, Pick up an\nopen shift, Fill a form, Complete a task, Register for an event), each\nan existing route with its app's own gate.\n\n### Scope ceiling\n\nTakes `read:search` (what the MangoApps app sign-ins grant) or\n`read:my_data`, as `GET /search` does. The same ceiling applies: an\narea that needs its own scope\n(`read:payroll`, `read:hr_file_manager`, `read:attendance`, …) is\ndropped before ranking, so it never reaches `best_match` or `rows`\neither, unless the token holds that scope.\n",
        "operationId": "searchLookahead",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "What the caller has typed so far — at least 2 characters, at most 200 (longer is cut).",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 200
            },
            "example": "policy"
          },
          {
            "name": "app",
            "in": "query",
            "required": false,
            "description": "The app the search box is in (its slug, e.g. `service-desk`). That app's own areas join the fast ones, so searching from inside an app finds its records without \"See all\". Unknown slugs are ignored.",
            "schema": {
              "type": "string"
            },
            "example": "policy-hub"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per section in `sections[].items` (1–50). `count` always says how many matched.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 3
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The as-you-type list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query",
                    "rows",
                    "sections",
                    "shortcuts",
                    "meta"
                  ],
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The query as searched — `q`, trimmed.",
                      "example": "policy"
                    },
                    "live_answer": {
                      "type": "object",
                      "nullable": true,
                      "description": "A structured fact the query asked for, when there is one.",
                      "additionalProperties": true
                    },
                    "best_match": {
                      "allOf": [
                        {
                          "type": "object",
                          "description": "One result. Fields a row has no value for are omitted.",
                          "properties": {
                            "id": {
                              "description": "The record's id within its area (people rows carry the user id).",
                              "oneOf": [
                                {
                                  "type": "integer"
                                },
                                {
                                  "type": "string"
                                }
                              ]
                            },
                            "title": {
                              "type": "string"
                            },
                            "url": {
                              "type": "string"
                            },
                            "subtitle": {
                              "type": "string"
                            },
                            "icon": {
                              "type": "string"
                            },
                            "section": {
                              "type": "string",
                              "description": "The area a ranked row (`best_match`, `top`) came from."
                            },
                            "section_title": {
                              "type": "string"
                            },
                            "updated_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "owner": {
                              "type": "string"
                            },
                            "owner_id": {
                              "type": "integer"
                            },
                            "version": {
                              "type": "string"
                            },
                            "event_dated": {
                              "type": "boolean",
                              "description": "True when `updated_at` is when the thing happens (a shift's start), not when it was last edited."
                            },
                            "outdated": {
                              "type": "boolean"
                            },
                            "takes_effect_on": {
                              "type": "string",
                              "format": "date"
                            },
                            "semantic": {
                              "type": "boolean",
                              "description": "Found by meaning rather than by its words."
                            },
                            "promoted": {
                              "type": "boolean",
                              "description": "An admin-promoted result for this query."
                            },
                            "external_host": {
                              "type": "string"
                            },
                            "snippet": {
                              "type": "string",
                              "description": "An external source's excerpt."
                            },
                            "passage": {
                              "type": "string",
                              "maxLength": 320,
                              "description": "Where the row matched, as the results page draws it under the row — the sentence a Messages result's word is in (its subject, for a thread matched on it), a page's matched paragraph, or the record's summary (a summary's markdown is stripped). Text to draw as-is, never HTML; at most 320 characters."
                            },
                            "passage_term": {
                              "type": "string",
                              "description": "The words to mark in `passage` (case-insensitive), as the web marks them. Sent only with a passage, and only when it is at least two characters."
                            },
                            "score": {
                              "type": "number"
                            },
                            "reasons": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "applies_chip": {
                              "type": "string",
                              "description": "A policy row's applicability chip, e.g. \"Applies to you · Sales\" or \"For California employees\"."
                            },
                            "status": {
                              "type": "string",
                              "description": "What the caller already did with the row, in place of its action — \"Pick-up requested · pending approval\", \"You're registered\", \"On the waitlist\" or \"You attended\". Absent otherwise."
                            },
                            "actions": {
                              "type": "array",
                              "description": "What the caller can do from the row (e.g. acknowledge, open). Each is `{label, url, method, primary, confirm}`. A `get` action is a page to open. A `post` action is the web app's own form endpoint: it needs a signed-in browser session and its CSRF token, not a bearer token — open the row's own `url` in a web view, or call that app's own API for the action.",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            },
                            "person": {
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/SearchPersonCard"
                                }
                              ],
                              "description": "On every People row — in the People section and as the best match or a top row."
                            }
                          }
                        }
                      ],
                      "nullable": true,
                      "description": "The single strongest row; null when nothing scores, or when it only repeats the live answer."
                    },
                    "rows": {
                      "type": "array",
                      "description": "The flat lookahead list (at most 8): a linkable live answer (`section: live`), the best match and the ranked rows, then the rest in area order — one row per destination and per visible line.",
                      "items": {
                        "type": "object",
                        "description": "One result. Fields a row has no value for are omitted.",
                        "properties": {
                          "id": {
                            "description": "The record's id within its area (people rows carry the user id).",
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "string"
                              }
                            ]
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "subtitle": {
                            "type": "string"
                          },
                          "icon": {
                            "type": "string"
                          },
                          "section": {
                            "type": "string",
                            "description": "The area a ranked row (`best_match`, `top`) came from."
                          },
                          "section_title": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "owner": {
                            "type": "string"
                          },
                          "owner_id": {
                            "type": "integer"
                          },
                          "version": {
                            "type": "string"
                          },
                          "event_dated": {
                            "type": "boolean",
                            "description": "True when `updated_at` is when the thing happens (a shift's start), not when it was last edited."
                          },
                          "outdated": {
                            "type": "boolean"
                          },
                          "takes_effect_on": {
                            "type": "string",
                            "format": "date"
                          },
                          "semantic": {
                            "type": "boolean",
                            "description": "Found by meaning rather than by its words."
                          },
                          "promoted": {
                            "type": "boolean",
                            "description": "An admin-promoted result for this query."
                          },
                          "external_host": {
                            "type": "string"
                          },
                          "snippet": {
                            "type": "string",
                            "description": "An external source's excerpt."
                          },
                          "passage": {
                            "type": "string",
                            "maxLength": 320,
                            "description": "Where the row matched, as the results page draws it under the row — the sentence a Messages result's word is in (its subject, for a thread matched on it), a page's matched paragraph, or the record's summary (a summary's markdown is stripped). Text to draw as-is, never HTML; at most 320 characters."
                          },
                          "passage_term": {
                            "type": "string",
                            "description": "The words to mark in `passage` (case-insensitive), as the web marks them. Sent only with a passage, and only when it is at least two characters."
                          },
                          "score": {
                            "type": "number"
                          },
                          "reasons": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "applies_chip": {
                            "type": "string",
                            "description": "A policy row's applicability chip, e.g. \"Applies to you · Sales\" or \"For California employees\"."
                          },
                          "status": {
                            "type": "string",
                            "description": "What the caller already did with the row, in place of its action — \"Pick-up requested · pending approval\", \"You're registered\", \"On the waitlist\" or \"You attended\". Absent otherwise."
                          },
                          "actions": {
                            "type": "array",
                            "description": "What the caller can do from the row (e.g. acknowledge, open). Each is `{label, url, method, primary, confirm}`. A `get` action is a page to open. A `post` action is the web app's own form endpoint: it needs a signed-in browser session and its CSRF token, not a bearer token — open the row's own `url` in a web view, or call that app's own API for the action.",
                            "items": {
                              "type": "object",
                              "additionalProperties": true
                            }
                          },
                          "person": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/SearchPersonCard"
                              }
                            ],
                            "description": "On every People row — in the People section and as the best match or a top row."
                          }
                        }
                      }
                    },
                    "shortcuts": {
                      "type": "array",
                      "description": "Pages to go to (\"Go to\").",
                      "items": {
                        "type": "object",
                        "properties": {
                          "title": {
                            "type": "string",
                            "example": "Policy Hub"
                          },
                          "path": {
                            "type": "string",
                            "example": "/apps/policy-hub"
                          }
                        }
                      }
                    },
                    "sections": {
                      "type": "array",
                      "description": "The results by area, in the engine's order.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "policy_hub"
                          },
                          "title": {
                            "type": "string",
                            "example": "Policies"
                          },
                          "count": {
                            "type": "integer",
                            "description": "Rows in this section (`items` holds at most `limit` of them)."
                          },
                          "total": {
                            "type": "integer",
                            "description": "The area's full match count, only when it capped its rows (\"8 of 46\"). On `home.for_you`, the full number owed (policies to acknowledge, tasks due) when the home shows only the first few."
                          },
                          "see_all_url": {
                            "type": "string",
                            "description": "The app's own search for this query, where it has one. On `home.for_you`, the app's own list of everything owed (Policy Hub's pending acknowledgments, My Tasks, open shifts)."
                          },
                          "see_all_title": {
                            "type": "string"
                          },
                          "also_in": {
                            "type": "boolean",
                            "description": "True when the page folds this section into its \"Also in\" block — a late section (after the first six) holding one row, when at least five such sections exist. People never fold."
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/SearchRow"
                            }
                          }
                        }
                      }
                    },
                    "ask_ai_enabled": {
                      "type": "boolean",
                      "description": "Whether Ask AI is available to the caller."
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "elapsed_ms": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The engine's time for this lookahead."
                        },
                        "total": {
                          "type": "integer",
                          "description": "Rows across `sections` (before `limit`)."
                        },
                        "ranked": {
                          "type": "boolean",
                          "description": "False when ranking was skipped after an error — `rows` is then the plain area-order list and `best_match` and `live_answer` are null."
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "policy": {
                    "summary": "GET /search/lookahead?q=policy",
                    "value": {
                      "query": "policy",
                      "live_answer": null,
                      "best_match": {
                        "id": 12,
                        "title": "Data Privacy Policy",
                        "url": "/apps/my_bookmarks/12",
                        "section": "my_bookmarks",
                        "section_title": "My bookmarks",
                        "reasons": [
                          "Yours"
                        ]
                      },
                      "rows": [
                        {
                          "id": 12,
                          "title": "Data Privacy Policy",
                          "url": "/apps/my_bookmarks/12",
                          "section": "my_bookmarks",
                          "section_title": "My bookmarks"
                        },
                        {
                          "id": 42,
                          "title": "Paid Time Off Policy",
                          "url": "/apps/policy-hub/policies/42",
                          "subtitle": "Leave · v3",
                          "section": "policy_hub",
                          "section_title": "Policies",
                          "actions": [
                            {
                              "label": "Acknowledge",
                              "url": "/apps/policy-hub/acknowledge/42/view",
                              "method": "get",
                              "primary": true
                            }
                          ]
                        }
                      ],
                      "shortcuts": [
                        {
                          "title": "Policy Hub",
                          "path": "/apps/policy-hub"
                        }
                      ],
                      "sections": [
                        {
                          "key": "policy_hub",
                          "title": "Policies",
                          "count": 5,
                          "see_all_url": "/apps/policy-hub/policies?search=policy",
                          "see_all_title": "Policy Hub",
                          "items": [
                            {
                              "id": 42,
                              "title": "Paid Time Off Policy",
                              "url": "/apps/policy-hub/policies/42",
                              "subtitle": "Leave · v3",
                              "actions": [
                                {
                                  "label": "Acknowledge",
                                  "url": "/apps/policy-hub/acknowledge/42/view",
                                  "method": "get",
                                  "primary": true
                                }
                              ]
                            },
                            {
                              "id": 43,
                              "title": "Anti-Harassment & Non-Discrimination Policy",
                              "url": "/apps/policy-hub/policies/43"
                            },
                            {
                              "id": 44,
                              "title": "Attendance & Punctuality Policy",
                              "url": "/apps/policy-hub/policies/44"
                            }
                          ]
                        }
                      ],
                      "ask_ai_enabled": true,
                      "meta": {
                        "elapsed_ms": 154,
                        "total": 9,
                        "ranked": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`q` is missing or one character (`invalid_query`).",
            "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 holds neither `read:search` nor `read:my_data` (`insufficient_permissions`).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search/saved_searches": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "List the caller's saved searches",
        "description": "The caller's own saved searches in this organisation, newest saved\nfirst. The order never changes when a search is edited later, as on the\nSaved searches page. A person keeps at most `meta.max` (30), so the list\nis one page.\n\nTakes what `GET /search` takes — `read:search` (the app sign-ins) or\n`read:my_data` (an AI connector) — since its page before you type already\nlists them.\n\nAn alerting search whose alerts cannot find anything says why:\n`alerts_muted` (the person's or the organisation's notification\nsettings) or `area_closed` — `excluded` when the one area it is\nnarrowed to (`filters.in`) is switched off in the organisation's\nSearch settings, `no_access` when the person can no longer open that\narea's app.\n",
        "operationId": "listSavedSearches",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved_searches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "query": {
                            "type": "string"
                          },
                          "filters": {
                            "$ref": "#/components/schemas/SearchFilters"
                          },
                          "alerting": {
                            "type": "boolean"
                          },
                          "alert_cadence": {
                            "type": "string",
                            "enum": [
                              "off",
                              "daily",
                              "weekly"
                            ]
                          },
                          "cadence_label": {
                            "type": "string",
                            "example": "Weekly"
                          },
                          "last_alerted_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the last alert went out. Absent until one has (a search saved with alerts off never has one)."
                          },
                          "last_match_count": {
                            "type": "integer",
                            "description": "The new results that last alert reported (\"4 new\"). Only alongside `last_alerted_at`.",
                            "example": 4
                          },
                          "last_checked_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the alert pass last reached this search, whether or not it found any — \"Checked 2 hours ago · nothing new yet\". A daily alert checked days ago is not running. A pass also stamps it when it skips a muted alert, so read it beside `alerts_muted`."
                          },
                          "alerts_muted": {
                            "type": "string",
                            "enum": [
                              "mine",
                              "organisation"
                            ],
                            "description": "Present on an alerting search whose alerts will not be delivered — `mine` when the person muted Search notices in their notification settings, `organisation` when the organisation's settings turn them off."
                          },
                          "area_closed": {
                            "type": "string",
                            "enum": [
                              "excluded",
                              "no_access"
                            ],
                            "description": "Present on an alerting search narrowed (`filters.in`) to an area the organisation switched off in Search settings (`excluded`) or whose app the person can no longer open (`no_access`); such a search finds nothing on any pass, so it is not checked."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "example": 2
                        },
                        "max": {
                          "type": "integer",
                          "description": "How many searches one person may keep.",
                          "example": 30
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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 holds neither `read:search` nor `read:my_data` (`insufficient_permissions`).",
            "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": [
          "Search"
        ],
        "summary": "Save a search",
        "description": "\"Save this search\" from the results page: the query and the filters the\nresults were shown with (`in`, `since` or its alias `when`, `mine`),\nstored as the page stores them. So `GET /search` with the same query and\nfilters answers this row as `saved_search`, and its `id` is what\n`DELETE` takes.\n\nAlerts are off unless `alert_cadence` asks for `daily` or `weekly`; any\nother value is off. What already matches when the alert is turned on is\nnever reported as new: that baseline is taken in the background right\nafter a save with an alert, or when `PATCH` turns the alert on later.\nAlerts watch OneMango content, not connected sources.\n\nThe same query with different filters is a different saved search.\nSaving one that is already saved answers `409 already_saved` with the\nexisting row in `error.details.saved_search`.\n\nNeeds `write:personal`.\n",
        "operationId": "saveSearch",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "q"
                ],
                "properties": {
                  "q": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 200,
                    "example": "leave policy"
                  },
                  "in": {
                    "type": "string",
                    "description": "The In filter — a section key from `GET /search`'s `filter_options.in`.",
                    "example": "policy_hub"
                  },
                  "since": {
                    "type": "string",
                    "enum": [
                      "30d",
                      "90d",
                      "year"
                    ],
                    "description": "The When filter."
                  },
                  "when": {
                    "type": "string",
                    "enum": [
                      "30d",
                      "90d",
                      "year"
                    ],
                    "description": "Alias for `since`; stored as `since`."
                  },
                  "mine": {
                    "description": "The Only mine filter — `1` or `true`.",
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "1",
                          "true"
                        ]
                      },
                      {
                        "type": "boolean"
                      }
                    ]
                  },
                  "alert_cadence": {
                    "type": "string",
                    "enum": [
                      "off",
                      "daily",
                      "weekly"
                    ],
                    "default": "off"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved_search": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "query": {
                          "type": "string"
                        },
                        "filters": {
                          "$ref": "#/components/schemas/SearchFilters"
                        },
                        "alerting": {
                          "type": "boolean"
                        },
                        "alert_cadence": {
                          "type": "string",
                          "enum": [
                            "off",
                            "daily",
                            "weekly"
                          ]
                        },
                        "cadence_label": {
                          "type": "string",
                          "example": "Weekly"
                        },
                        "last_alerted_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the last alert went out. Absent until one has (a search saved with alerts off never has one)."
                        },
                        "last_match_count": {
                          "type": "integer",
                          "description": "The new results that last alert reported (\"4 new\"). Only alongside `last_alerted_at`.",
                          "example": 4
                        },
                        "last_checked_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the alert pass last reached this search, whether or not it found any — \"Checked 2 hours ago · nothing new yet\". A daily alert checked days ago is not running. A pass also stamps it when it skips a muted alert, so read it beside `alerts_muted`."
                        },
                        "alerts_muted": {
                          "type": "string",
                          "enum": [
                            "mine",
                            "organisation"
                          ],
                          "description": "Present on an alerting search whose alerts will not be delivered — `mine` when the person muted Search notices in their notification settings, `organisation` when the organisation's settings turn them off."
                        },
                        "area_closed": {
                          "type": "string",
                          "enum": [
                            "excluded",
                            "no_access"
                          ],
                          "description": "Present on an alerting search narrowed (`filters.in`) to an area the organisation switched off in Search settings (`excluded`) or whose app the person can no longer open (`no_access`); such a search finds nothing on any pass, so it is not checked."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "The sentence the web page shows after a save.",
                      "example": "Saved “leave policy”. Turn on alerts under Saved searches to hear about new results. Alerts watch OneMango content, not connected sources."
                    }
                  }
                }
              }
            }
          },
          "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 `write:personal` (`insufficient_permissions`).",
            "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": "This search, with these filters, is already saved (`already_saved`, \"You've already saved this search.\"). `error.details.saved_search` is the saved row.",
            "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 saved (`invalid_saved_search`), with the page's own sentence — e.g. \"Type at least 2 characters before saving a search.\", or the limit of 30 saved searches reached.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/search/saved_searches/{id}": {
      "patch": {
        "tags": [
          "Search"
        ],
        "summary": "Rename a saved search or set its alert",
        "description": "What the Saved searches page's Rename and alert controls do. Send\n`name`, `alert_cadence`, or both; only what is sent changes. `PUT`\ndoes the same.\n\nTurning an alert on (from `off` to `daily` or `weekly`) means \"new\nfrom now on\": what the search matches at that moment is taken as the\nbaseline in the background and is never reported as new. Switching\nbetween `daily` and `weekly` keeps the baseline. Alerts watch\nOneMango content, not connected sources.\n\nAny other id, including another person's, answers `404` and changes\nnothing. Needs `write:personal`.\n",
        "operationId": "updateSavedSearch",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The saved search's id (from the list, a save, or `GET /search`'s `saved_search`).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "The name shown instead of the query; blank shows the query.",
                    "example": "Leave questions"
                  },
                  "alert_cadence": {
                    "type": "string",
                    "enum": [
                      "off",
                      "daily",
                      "weekly"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved_search": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "query": {
                          "type": "string"
                        },
                        "filters": {
                          "$ref": "#/components/schemas/SearchFilters"
                        },
                        "alerting": {
                          "type": "boolean"
                        },
                        "alert_cadence": {
                          "type": "string",
                          "enum": [
                            "off",
                            "daily",
                            "weekly"
                          ]
                        },
                        "cadence_label": {
                          "type": "string",
                          "example": "Weekly"
                        },
                        "last_alerted_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the last alert went out. Absent until one has (a search saved with alerts off never has one)."
                        },
                        "last_match_count": {
                          "type": "integer",
                          "description": "The new results that last alert reported (\"4 new\"). Only alongside `last_alerted_at`.",
                          "example": 4
                        },
                        "last_checked_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the alert pass last reached this search, whether or not it found any — \"Checked 2 hours ago · nothing new yet\". A daily alert checked days ago is not running. A pass also stamps it when it skips a muted alert, so read it beside `alerts_muted`."
                        },
                        "alerts_muted": {
                          "type": "string",
                          "enum": [
                            "mine",
                            "organisation"
                          ],
                          "description": "Present on an alerting search whose alerts will not be delivered — `mine` when the person muted Search notices in their notification settings, `organisation` when the organisation's settings turn them off."
                        },
                        "area_closed": {
                          "type": "string",
                          "enum": [
                            "excluded",
                            "no_access"
                          ],
                          "description": "Present on an alerting search narrowed (`filters.in`) to an area the organisation switched off in Search settings (`excluded`) or whose app the person can no longer open (`no_access`); such a search finds nothing on any pass, so it is not checked."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "The sentence the web page shows after the change.",
                      "example": "Updated “leave policy” — daily alerts, for results new from now on. Alerts watch OneMango content, not connected sources."
                    }
                  }
                }
              }
            }
          },
          "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 `write:personal` (`insufficient_permissions`).",
            "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 one of the caller's saved searches (`not_found`, \"That saved search is gone.\").",
            "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 changed (`invalid_saved_search`), with the page's own sentence — e.g. \"Choose No alerts, Daily or Weekly.\" for an unknown cadence.",
            "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": [
          "Search"
        ],
        "summary": "Remove a saved search",
        "description": "Removes one of the caller's own saved searches. Any other id, including\nanother person's, answers `404` and removes nothing. Needs `write:personal`.\n",
        "operationId": "removeSavedSearch",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ServiceAccountAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The saved search's id (from the list, a save, or `GET /search`'s `saved_search`).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "boolean",
                      "example": true
                    },
                    "id": {
                      "type": "integer",
                      "example": 3
                    },
                    "message": {
                      "type": "string",
                      "example": "Removed “leave policy”."
                    }
                  }
                }
              }
            }
          },
          "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 `write:personal` (`insufficient_permissions`).",
            "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 one of the caller's saved searches (`not_found`, \"That saved search is gone.\").",
            "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": "The row could not be removed (`not_removed`).",
            "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** — a Libraries admin or a place-limited\n   one, precisely what the index's `can_disable` flag reports. Business\n   admins, super admins, **per-app Libraries admins** and place-limited\n   Libraries admins qualify; a manager with no Libraries grant does not.\n   Otherwise `403 forbidden`.\n4. **This library** — `Libraries::Access#can_toggle_library?`, the\n   web's `toggle_enabled` gate: a place-limited admin only for a library\n   in their reach. Otherwise `403 forbidden`.\n\nGate 3 is **app-wide, not per-library**, and is answered BEFORE the\nlibrary is resolved. So a caller with no Libraries admin grant gets\n`403` for a real id and for a nonexistent one alike, and cannot\nenumerate library ids by reading `403`-vs-`404`. A library belonging to\nanother tenant is `404`, never `403` — a `403` would confirm the id is\nreal somewhere.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:libraries` scope.\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** — a Libraries admin or a place-limited\n   one, precisely what the index's `can_disable` flag reports. Business\n   admins, super admins, **per-app Libraries admins** and place-limited\n   Libraries admins qualify; a manager with no Libraries grant does not.\n   Otherwise `403 forbidden`.\n4. **This library** — `Libraries::Access#can_toggle_library?`, the\n   web's `toggle_enabled` gate: a place-limited admin only for a library\n   in their reach. Otherwise `403 forbidden`.\n\nGate 3 is **app-wide, not per-library**, and is answered BEFORE the\nlibrary is resolved. So a caller with no Libraries admin grant gets\n`403` for a real id and for a nonexistent one alike, and cannot\nenumerate library ids by reading `403`-vs-`404`. A library belonging to\nanother tenant is `404`, never `403` — a `403` would confirm the id is\nreal 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`). An item whose library the caller is in the audience of but which has been switched off answers `library_disabled` instead, with the message \"\\\"<library name>\\\" has been disabled and isn't available right now.\""
          }
        }
      },
      "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`, or `library_disabled` when only the library's off switch keeps them out — see `POST`)."
          }
        }
      }
    },
    "/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 is saved in a library the caller can open. A client that instead took `libraries.length` as the group count would report zero groups for every page after the first. Saves in libraries the caller can no longer open are not counted here; see `hidden_count`.",
                      "example": 2
                    },
                    "hidden_count": {
                      "type": "integer",
                      "description": "How many of the caller's saved items still exist but sit in a library they can no longer open (its audience narrowed, or it was disabled). A count only; the items themselves are never listed. Present on every page.",
                      "example": 0
                    },
                    "hidden_note": {
                      "type": "string",
                      "nullable": true,
                      "description": "The sentence the web and phone Bookmarked lists print for `hidden_count` (\"2 saved items are in libraries you can no longer open.\"); null when `hidden_count` is 0.",
                      "example": null
                    },
                    "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,
                          "can_delete": 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,
                          "can_delete": 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,
                      "hidden_count": 0,
                      "hidden_note": null,
                      "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,
                          "can_delete": 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,
                      "hidden_count": 0,
                      "hidden_note": null,
                      "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,
                      "hidden_count": 0,
                      "hidden_note": null,
                      "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"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/sites/{site_id}/follow": {
      "parameters": [
        {
          "name": "site_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Whether the caller follows a site",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Follow state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "following": {
                      "type": "boolean"
                    },
                    "follows_enabled": {
                      "type": "boolean",
                      "description": "The tenant's `enable_site_follows` switch. When false, POST is refused."
                    },
                    "site": {
                      "$ref": "#/components/schemas/SitesRef"
                    }
                  }
                },
                "example": {
                  "following": true,
                  "follows_enabled": true,
                  "site": {
                    "id": 12,
                    "name": "People Team"
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such site in this organization, or one the caller cannot open."
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Follow a site",
        "description": "Be notified when pages are published in this site. Idempotent. Refused with 403 `follows_disabled` when the tenant has follows switched off, and 404 `not_found` when the caller cannot open the site.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retried request with the same key replays the first answer instead of writing again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Following (new or existing follow)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "following": {
                      "type": "boolean",
                      "example": true
                    },
                    "followed_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "site": {
                      "$ref": "#/components/schemas/SitesRef"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`follows_disabled`."
          },
          "404": {
            "description": "`not_found` — no such site, or one the caller cannot open."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Stop following a site",
        "description": "Removes the caller's own follow. Never gated on access or on the follows switch, and answers the same whether or not a follow existed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retried request with the same key replays the first answer instead of writing again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Not following",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "following": {
                      "type": "boolean",
                      "example": false
                    },
                    "site_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/pages/{page_id}/watch": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Whether the caller watches a page",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Watch state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watching": {
                      "type": "boolean"
                    },
                    "follows_enabled": {
                      "type": "boolean"
                    },
                    "page": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page. A caller who cannot know the page exists (outside its site or its audience, or a draft never published to them, and not its approver) gets 404 `not_found` instead, as for an unknown id."
          },
          "404": {
            "description": "`not_found`."
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Watch a page",
        "description": "Be notified when this page is updated. Idempotent. Needs the page open to the caller (site access, the page's status and its audience) and the tenant's `enable_site_follows` switch on.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retried request with the same key replays the first answer instead of writing again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Watching (new or existing watch)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watching": {
                      "type": "boolean",
                      "example": true
                    },
                    "watched_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "page": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`follows_disabled` or `access_denied`."
          },
          "404": {
            "description": "`not_found`."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Stop watching a page",
        "description": "Removes the caller's own watch. Never gated on access or on the follows switch.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retried request with the same key replays the first answer instead of writing again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Not watching",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watching": {
                      "type": "boolean",
                      "example": false
                    },
                    "page_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/pages/{page_id}/comments": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "A page's comment thread",
        "description": "Top-level comments, oldest first, each with its replies nested (one level — replies cannot be replied to). Needs the page open to the caller and the tenant's `enable_comments` switch on (403 `comments_disabled`).",
        "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comments",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesComment"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "description": "Pagination over TOP-LEVEL comments, plus `comment_count` — every comment on the page, replies included.",
                          "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"
                            },
                            "comment_count": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "`access_denied` or `comments_disabled`."
          },
          "404": {
            "description": "`not_found`."
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Comment on a page, or reply to a comment",
        "description": "Posts through the same writer as the desktop and phone forms, so it notifies the same people: the page's author, recent commenters on the page, and anyone @mentioned who can open the page. A mention is `@[Name](mention:<user id>)` (what the web composer's @ picker writes), or typed \"@First Last\" / \"@name@example.com\". `parent_id` makes it a reply; it must be a top-level comment on the same page.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retried request with the same key replays the first answer instead of writing again.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "parent_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/SitesComment"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` or `comments_disabled`."
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different body; or `idempotency_in_progress` — a request with this key is still running (retry after the `Retry-After` seconds to get its answer replayed). Both in the `error` envelope."
          },
          "422": {
            "description": "Two shapes. A blank `body` answers the `error` envelope with code `invalid_body`. A body the comment itself refuses — longer than 10,000 characters, a `parent_id` that is a reply, or one on another page — answers an `errors` list, one entry per problem, each with code `validation_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "error"
                      ],
                      "properties": {
                        "error": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "example": "invalid_body"
                            },
                            "message": {
                              "type": "string",
                              "example": "body can't be blank"
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "errors"
                      ],
                      "properties": {
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "field": {
                                "type": "string",
                                "example": "parent_id"
                              },
                              "message": {
                                "type": "string"
                              },
                              "code": {
                                "type": "string",
                                "example": "validation_error"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sites/pages/{page_id}/comments/{id}": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The comment's id (a top-level comment or a reply) on this page.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Sites"
        ],
        "summary": "Edit a comment",
        "description": "The web's Edit on a comment. The comment's author alone — nobody edits someone else's words; offered where the comment's `can_edit` is true. Stamps `edited_at`, and notifies only the people the edit newly @mentions (anyone the comment already named is not told again). `PUT` is accepted too.\n\nRefusals, in order: a page outside the caller's company, or a comment not on this page, is 404 `not_found`; a page the caller cannot open is 403 `access_denied` (404 `not_found` when they cannot know it exists); comments switched off is 403 `comments_disabled`; anyone but the author is 403 `not_allowed`; a blank body is 422 `invalid_body`; a body the comment refuses (over 10,000 characters) is 422 with an `errors` list.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 10000,
                    "description": "The new body. Mentions as for POST."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Edited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/SitesComment"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Present when a typed \"@First Last\" names several colleagues and so reached nobody.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token."
          },
          "403": {
            "description": "`access_denied`, `comments_disabled` or `not_allowed`; a token without `write:sites` is refused too."
          },
          "404": {
            "description": "`not_found` — the page or the comment."
          },
          "422": {
            "description": "A blank `body` answers the `error` envelope with code `invalid_body`; a body the comment itself refuses answers an `errors` list, each entry with code `validation_error`."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Delete a comment",
        "description": "The web's Delete on a comment. Its author, a company admin or Sites app-admin, or an owner or editor of the page's site (who moderate what appears on it); offered where the comment's `can_delete` is true. Deleting a top-level comment deletes its replies with it. Unlike the other comment calls this does not need the tenant's comments switched on, as on the web, so a moderator can still remove a comment after comments went off. A retried DELETE with the same `Idempotency-Key` replays the first answer.\n\nRefusals, in order: a page outside the caller's company, or a comment not on this page (or already deleted), is 404 `not_found`; a page the caller cannot open is 403 `access_denied` (404 `not_found` when they cannot know it exists); anyone else is 403 `not_allowed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Comment removed"
                    },
                    "id": {
                      "type": "integer",
                      "description": "The deleted comment's id."
                    },
                    "comment_count": {
                      "type": "integer",
                      "description": "Every comment left on the page, replies included."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token."
          },
          "403": {
            "description": "`access_denied` or `not_allowed`; a token without `write:sites` is refused too."
          },
          "404": {
            "description": "`not_found` — the page or the comment."
          }
        }
      }
    },
    "/sites/following": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Sites the caller follows",
        "description": "Most recently followed first; only sites the caller can still open. 403 `follows_disabled` when the tenant has follows switched off.",
        "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Followed sites",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "followed_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "site": {
                                "$ref": "#/components/schemas/SitesListSiteRow"
                              }
                            }
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/SitesPagination"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "`follows_disabled`."
          }
        }
      }
    },
    "/sites/watching": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Pages the caller watches",
        "description": "Most recently watched first; only pages the caller can still open. 403 `follows_disabled` when the tenant has follows switched off.",
        "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Watched pages",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "watched_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "page": {
                                "$ref": "#/components/schemas/SitesPageSummary"
                              }
                            }
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/SitesPagination"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "`follows_disabled`."
          }
        }
      }
    },
    "/sites/to_confirm": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "To confirm — must-read pages waiting on the caller",
        "description": "The \"To confirm\" tab of Saved & following (the web's `/apps/sites/bookmarks?tab=confirm` and its phone twin, loaded the same way): pages whose live version asked the caller to confirm they have read it, and that they have not confirmed, newest ask first. Only pages the caller can open now; sample pages never ask.\n\nEach row is the page and its `read_confirmation` — the same block `GET /sites/pages/{id}` carries: the version asking, the caller's own confirm-by date (`due_on`; null when the ask has no date) and whether it is `overdue`. The web row reads \"in <site> · version N · confirm by <date>\", or an Overdue badge past it. The list never confirms: open the page and `POST /sites/pages/{page_id}/acknowledgement`; the page then leaves this list. Always answered — a must-read ask has no tenant switch. `total_count` is the number the tab's label carries.",
        "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pages waiting on the caller's confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "page": {
                                "$ref": "#/components/schemas/SitesPageSummary"
                              },
                              "version_published_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the version asking was published."
                              },
                              "read_confirmation": {
                                "type": "object",
                                "description": "Whether the live version of a page asks THIS caller to confirm reading it. `{ requested: false }` when it does not.",
                                "properties": {
                                  "requested": {
                                    "type": "boolean"
                                  },
                                  "acknowledged": {
                                    "type": "boolean"
                                  },
                                  "acknowledged_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "nullable": true
                                  },
                                  "version_number": {
                                    "type": "integer"
                                  },
                                  "due_on": {
                                    "type": "string",
                                    "format": "date",
                                    "nullable": true
                                  },
                                  "overdue": {
                                    "type": "boolean"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/SitesPagination"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "page": {
                        "id": 812,
                        "title": "Forklift safety",
                        "display_title": "Forklift safety",
                        "slug": "forklift-safety",
                        "page_type": "content",
                        "status": "published",
                        "published_at": "2026-10-07T14:00:00Z",
                        "site": {
                          "id": 41,
                          "name": "New York Office",
                          "display_name": "New York Office",
                          "slug": "new-york-office",
                          "site_type": "location",
                          "status": "published",
                          "icon": "fa-store",
                          "icon_color": null,
                          "tile_color": "#0f7b8a"
                        }
                      },
                      "version_published_at": "2026-10-07T14:00:00Z",
                      "read_confirmation": {
                        "requested": true,
                        "acknowledged": false,
                        "acknowledged_at": null,
                        "version_number": 4,
                        "due_on": "2026-10-14",
                        "overdue": true
                      }
                    }
                  ],
                  "meta": {
                    "total_count": 1,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "total_count": 1
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/sites/recently_read": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Recent — pages the caller opened, and what changed in their sites",
        "description": "The Sites Recent screen as data (the web's Sites › Recent and its phone twin, loaded the same way), in two sections.\n\n`items` — \"Pages you opened\": the caller's latest read of each page in the last 90 days, newest first — one row per page however often it was opened, paged. A read is recorded whenever the caller opens a page on the web, on the phone, or through `GET /sites/pages/{id}` (not when an editor is served their unpublished draft there).\n\n`updated_in_your_sites` — \"Updated in sites for you\": up to 5 pages that changed for readers in the last 14 days on the caller's own sites (a member of it, following it, or one aimed at them), newest change first, each saying how it changed (`change.kind`) and, on a republish, the author's \"What changed?\" note. Unpaged — the same rows on every page of `items`, as the web draws them.\n\nBoth sections list only pages the caller can open now, so each role sees its own rows from the same request: an employee the pages their audience admits, a site editor also their sites' drafts (`status: draft`), a Sites admin every site. Each row's site carries its icon, the colour its tile paints (`tile_color`) and its parent site — named only when the caller may open that parent. Reading history is recorded only while the tenant's Page Analytics (`enable_analytics`) is on; with it off this answers 403 `analytics_disabled` rather than an empty list.",
        "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recently read pages",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "last_read_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "page": {
                                "$ref": "#/components/schemas/SitesRecentPage"
                              }
                            }
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/SitesPagination"
                        },
                        "updated_in_your_sites": {
                          "type": "array",
                          "description": "What changed lately in the caller's own sites (at most 5, the last 14 days), newest change first.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "changed_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When a reader last saw the page change — its last publish when live, else its last edit."
                              },
                              "change": {
                                "type": "object",
                                "properties": {
                                  "kind": {
                                    "type": "string",
                                    "enum": [
                                      "published",
                                      "republished",
                                      "updated"
                                    ],
                                    "description": "`published` — the page went live (its first publish); `republished` — published again; `updated` — not live, edited (a draft only its editors open)."
                                  },
                                  "note": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The author's \"What changed?\" note on a republish; null when they wrote none, and on every other kind."
                                  }
                                }
                              },
                              "page": {
                                "$ref": "#/components/schemas/SitesRecentPage"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "last_read_at": "2026-10-17T09:12:00Z",
                      "page": {
                        "id": 812,
                        "title": "Forklift safety",
                        "display_title": "Forklift safety",
                        "slug": "forklift-safety",
                        "page_type": "content",
                        "published_at": "2026-10-07T14:00:00Z",
                        "status": "published",
                        "updated_at": "2026-10-07T14:00:00Z",
                        "site": {
                          "id": 41,
                          "name": "New York Office",
                          "display_name": "New York Office",
                          "slug": "new-york-office",
                          "site_type": "location",
                          "status": "published",
                          "icon": "fa-store",
                          "icon_color": null,
                          "tile_color": "#0f7b8a",
                          "parent": {
                            "id": 7,
                            "name": "Operations",
                            "display_name": "Operations"
                          }
                        }
                      }
                    }
                  ],
                  "meta": {
                    "total_count": 1,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "total_count": 1,
                  "updated_in_your_sites": [
                    {
                      "changed_at": "2026-10-07T14:00:00Z",
                      "change": {
                        "kind": "republished",
                        "note": "Added the battery-charge check"
                      },
                      "page": {
                        "id": 812,
                        "title": "Forklift safety",
                        "display_title": "Forklift safety",
                        "slug": "forklift-safety",
                        "page_type": "content",
                        "published_at": "2026-10-07T14:00:00Z",
                        "status": "published",
                        "updated_at": "2026-10-07T14:00:00Z",
                        "site": {
                          "id": 41,
                          "name": "New York Office",
                          "display_name": "New York Office",
                          "slug": "new-york-office",
                          "site_type": "location",
                          "status": "published",
                          "icon": "fa-store",
                          "icon_color": null,
                          "tile_color": "#0f7b8a",
                          "parent": {
                            "id": 7,
                            "name": "Operations",
                            "display_name": "Operations"
                          }
                        }
                      }
                    }
                  ],
                  "unread_notification_count": 3
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "`analytics_disabled`."
          }
        }
      }
    },
    "/sites/pages/{page_id}/acknowledgement": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Confirm you have read a must-read page",
        "description": "Records the caller's \"I've read and understood this\" for the version of the page that is asking them. Idempotent: a repeat answers 201 with the same `acknowledged_at`. Honours `Idempotency-Key`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Confirmed (or already confirmed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "read_confirmation": {
                      "$ref": "#/components/schemas/SitesReadConfirmation"
                    },
                    "page": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "title": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "read_confirmation": {
                    "requested": true,
                    "acknowledged": true,
                    "acknowledged_at": "2026-09-23T14:05:00Z",
                    "version_number": 7,
                    "due_on": "2026-09-30",
                    "overdue": false
                  },
                  "page": {
                    "id": 1617,
                    "title": "Fire evacuation procedure"
                  }
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page."
          },
          "404": {
            "description": "`not_found` — no such page in this organization."
          },
          "409": {
            "description": "`save_failed` — the confirmation could not be recorded; retry."
          },
          "422": {
            "description": "`not_asked` — the live version of this page is not asking the caller to confirm (no ask on the page, a republish since the client read it, or the caller was not among the readers when it was published)."
          }
        }
      }
    },
    "/sites/pages/{page_id}/reactions": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Who reacted to a page",
        "description": "One row per reaction, newest first — a person who left 👍 and ❤️ is a row under each — with the person who left it. `summary` describes the whole page and is never narrowed by `emoji` or the page of rows: draw the sheet's tabs from it (\"All\" = `summary.total`, then one tab per `summary.counts` entry) and pass a tab's `emoji` back to list its rows.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "emoji",
            "in": "query",
            "required": false,
            "description": "One tab — the reaction's glyph (`👍`) or its catalog key (`like`).",
            "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reactions and the page's reaction summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "page_id": {
                      "type": "integer"
                    },
                    "summary": {
                      "$ref": "#/components/schemas/SitesReactionSummary"
                    },
                    "reactions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SitesReactionRow"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Reactions matching `emoji` (all of them without it)."
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "has_next_page": {
                          "type": "boolean"
                        },
                        "has_prev_page": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "page_id": 1617,
                  "summary": {
                    "total": 3,
                    "counts": [
                      {
                        "emoji": "👍",
                        "key": "like",
                        "label": "Like",
                        "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/like-1a2b.svg",
                        "count": 2
                      },
                      {
                        "emoji": "💡",
                        "key": "insightful",
                        "label": "Insightful",
                        "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/insightful-3c4d.svg",
                        "count": 1
                      }
                    ],
                    "mine": [
                      "👍"
                    ],
                    "available": [
                      {
                        "emoji": "👍",
                        "key": "like",
                        "label": "Like",
                        "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/like-1a2b.svg"
                      }
                    ]
                  },
                  "reactions": [
                    {
                      "emoji": "💡",
                      "key": "insightful",
                      "label": "Insightful",
                      "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/insightful-3c4d.svg",
                      "id": 90412,
                      "reacted_at": "2026-10-20T09:12:00Z",
                      "user": {
                        "id": 214,
                        "name": "Priya Shah",
                        "initials": "PS",
                        "job_title": "Store Manager",
                        "avatar_url": "https://officechat.workforce.mangoapps.com/rails/active_storage/representations/…"
                      }
                    }
                  ],
                  "meta": {
                    "total_count": 3,
                    "current_page": 1,
                    "total_pages": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or invalid token."
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page; `insufficient_permissions` — the token lacks `read:sites`."
          },
          "404": {
            "description": "`not_found` — no such page in this organization."
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "React to a page",
        "description": "Adds the caller's reaction. Idempotent: 201 when this call added it, 200 when the caller already had it (nothing changes). A new reaction must be one the company offers (`reactions.available` on the page).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "The reaction's glyph (`👍`) or its catalog key (`like`).",
                    "example": "👍"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesReactionWrite"
                }
              }
            }
          },
          "200": {
            "description": "The caller already had this reaction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesReactionWrite"
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page; `insufficient_permissions` — the token lacks `write:sites`."
          },
          "404": {
            "description": "`not_found` — no such page in this organization."
          },
          "422": {
            "description": "`emoji_required` — no emoji; `invalid_emoji` — not a reaction the company offers (`error.details.allowed` lists the ones it does)."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Remove your reaction from a page",
        "description": "Removes the caller's own reaction with this emoji. Idempotent: removing one the caller does not hold answers 200 with `removed: false`. A reaction the company no longer offers can still be removed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "emoji",
            "in": "query",
            "required": true,
            "description": "The reaction's glyph (`👍`) or its catalog key (`like`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed (or was not there)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SitesReactionWrite"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "removed": {
                          "type": "boolean",
                          "description": "False when the caller held no such reaction."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page; `insufficient_permissions` — the token lacks `write:sites`."
          },
          "404": {
            "description": "`not_found` — no such page in this organization."
          },
          "422": {
            "description": "`emoji_required` — no emoji."
          }
        }
      }
    },
    "/sites/pages/{page_id}/comments/{comment_id}/reactions": {
      "parameters": [
        {
          "name": "page_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "comment_id",
          "in": "path",
          "required": true,
          "description": "A comment or reply on this page.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "React to a page comment",
        "description": "Adds the caller's reaction to a comment or reply. Idempotent: 201 when this call added it, 200 when the caller already had it (nothing changes). A new reaction must be one the company offers (`reactions.available` in the answer).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji"
                ],
                "properties": {
                  "emoji": {
                    "type": "string",
                    "description": "The reaction's glyph (`👍`) or its catalog key (`like`).",
                    "example": "👍"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesCommentReactionWrite"
                },
                "example": {
                  "page_id": 1617,
                  "comment_id": 5120,
                  "emoji": "👍",
                  "reacted": true,
                  "reactions": {
                    "total": 2,
                    "counts": [
                      {
                        "emoji": "👍",
                        "key": "like",
                        "label": "Like",
                        "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/like-1a2b.svg",
                        "count": 2
                      }
                    ],
                    "mine": [
                      "👍"
                    ],
                    "available": [
                      {
                        "emoji": "👍",
                        "key": "like",
                        "label": "Like",
                        "icon_url": "https://officechat.workforce.mangoapps.com/assets/reactions/like-1a2b.svg"
                      }
                    ]
                  }
                }
              }
            }
          },
          "200": {
            "description": "The caller already had this reaction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesCommentReactionWrite"
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page; `comments_disabled` — comments are off for the organization; `insufficient_permissions` — the token lacks `write:sites`."
          },
          "404": {
            "description": "`not_found` — no such page in this organization, or no such comment on it."
          },
          "422": {
            "description": "`emoji_required` — no emoji; `invalid_emoji` — not a reaction the company offers (`error.details.allowed` lists the ones it does)."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Remove your reaction from a page comment",
        "description": "Removes the caller's own reaction with this emoji from a comment or reply. Idempotent: removing one the caller does not hold answers 200 with `removed: false`. A reaction the company no longer offers can still be removed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "emoji",
            "in": "query",
            "required": true,
            "description": "The reaction's glyph (`👍`) or its catalog key (`like`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed (or was not there)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SitesCommentReactionWrite"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "removed": {
                          "type": "boolean",
                          "description": "False when the caller held no such reaction."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "`access_denied` — the caller cannot open this page; `comments_disabled` — comments are off for the organization; `insufficient_permissions` — the token lacks `write:sites`."
          },
          "404": {
            "description": "`not_found` — no such page in this organization, or no such comment on it."
          },
          "422": {
            "description": "`emoji_required` — no emoji."
          }
        }
      }
    },
    "/sites/pages/{id}/mark_accurate": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The page's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Mark a page as still accurate",
        "description": "Says the page was checked today and is still right — the web's \"Still accurate\" (a Going stale row, the page's \"Not checked in …\" panel) and \"Mark as still accurate\" (the page's ⋮ menu). Readers see \"Checked <date>\" on the page's foot line, and it is off the Going stale list for the company's review interval. Not an edit: nothing about the page's content, last-updated time or history changes. A page with no owner gets the caller as its owner when they are one of the site's owners or editors (an admin from outside the site leaves it ownerless). Offered where `page.care.can_mark_accurate` is true. Marking again stamps the check again, so a retry is harmless.\n\nRefusals, in order: a page outside the caller's company is 404; a page the caller can neither open nor look after is 403 `access_denied` (nothing about its state is said); a site in the Trash is 403 `site_trashed` (said only to whoever looks after the page); a draft is 422 `not_published`; anyone but the page's owner, the site's owners and editors and admins is 403 `not_allowed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Marked as still accurate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesStillAccurate"
                },
                "example": {
                  "message": "Marked “Working Here” as still accurate — readers see it was checked today, and it's off the Going stale list for 180 days.",
                  "page": {
                    "id": 1617,
                    "title": "Working Here",
                    "site_id": 42
                  },
                  "still_accurate": {
                    "checked_at": "2026-10-21T09:30:00Z",
                    "checked_by": {
                      "id": 54,
                      "name": "Dana Whitfield"
                    },
                    "review_interval_days": 180,
                    "next_check_due_at": "2027-04-19T09:30:00Z",
                    "became_owner": false,
                    "owner": {
                      "id": 54,
                      "name": "Dana Whitfield"
                    }
                  },
                  "trust_line": {
                    "owner_name": "Dana Whitfield",
                    "label": "Checked",
                    "at": "2026-10-21T09:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`access_denied` — the caller can neither open this page nor look after it; `site_trashed` — the page's site is in the Trash (restore the site first); `not_allowed` — only the page's owner, the site's owners and editors, and admins can mark it; or the token lacks `write:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "Only this page's owner and the site's owners and editors can mark it as still accurate."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "422": {
            "description": "`not_published` — only a published page can be marked as still accurate.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_published",
                    "message": "Only a published page can be marked as still accurate."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/care_queues": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Pages that need care — every list with its count",
        "description": "One entry per list, in this order: `care` (every page with any problem), `owner` (needs an owner), `sources` (a linked policy, form or person changed), `links` (broken links), `stale` (going stale), `expiring` (expiring in 30 days) and `reading` (required reading behind). A list with pages carries the Dashboard row's `detail` line. Someone who looks after no page gets `looks_after_pages: false` and every count 0.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The lists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "care_queues": {
                      "type": "object",
                      "properties": {
                        "looks_after_pages": {
                          "type": "boolean"
                        },
                        "scope": {
                          "type": "string",
                          "enum": [
                            "all_sites",
                            "sites_and_pages_you_look_after"
                          ],
                          "description": "A Sites admin's lists cover every site."
                        },
                        "queues": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesCareQueue"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "care_queues": {
                    "looks_after_pages": true,
                    "scope": "sites_and_pages_you_look_after",
                    "queues": [
                      {
                        "key": "care",
                        "title": "Pages that need care",
                        "count": 3
                      },
                      {
                        "key": "owner",
                        "title": "Needs an owner",
                        "cta_label": "Choose owners",
                        "count": 0
                      },
                      {
                        "key": "sources",
                        "title": "Linked policy, form or person changed",
                        "cta_label": "Review updates",
                        "count": 1,
                        "detail": "Team Directory"
                      },
                      {
                        "key": "links",
                        "title": "Broken links",
                        "cta_label": "Fix links",
                        "count": 0
                      },
                      {
                        "key": "stale",
                        "title": "Going stale",
                        "cta_label": "Review pages",
                        "count": 1,
                        "detail": "Not published or checked in 180 days · oldest: How We Work, published about 1 year ago"
                      },
                      {
                        "key": "expiring",
                        "title": "Expiring in 30 days",
                        "cta_label": "Extend or archive",
                        "count": 1,
                        "detail": "Soonest: News & Updates, on Oct 27"
                      },
                      {
                        "key": "reading",
                        "title": "Required reading behind",
                        "cta_label": "Send reminders",
                        "count": 0
                      }
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "`app_disabled` / `app_access_denied` — no Sites app; `insufficient_permissions` — the token lacks `read:sites`."
          }
        }
      }
    },
    "/sites/care_queues/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The list's key.",
          "schema": {
            "type": "string",
            "enum": [
              "care",
              "owner",
              "sources",
              "links",
              "stale",
              "expiring",
              "reading"
            ]
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "One \"Pages that need care\" list",
        "description": "The list's pages in the web queue's order (`stale` oldest first, `expiring` soonest first, the rest by title), each with why it is here and the fixes the caller may take (`actions`). A fix is offered only where its door would take it, and a write only to a token holding `write:sites`: `mark_accurate` on every Going stale row; `extend_expiry`, `remind` and `review_refresh` to the site's owners and editors and admins, `review_refresh` only while the company's self-maintaining content is on or an update the editor asked for is waiting. `site_id` narrows the list to one site the caller may open (one they may not narrows it to nothing).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "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": "The list's rows.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesCareRow"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "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"
                                }
                              }
                            },
                            {
                              "type": "object",
                              "properties": {
                                "queue": {
                                  "$ref": "#/components/schemas/SitesCareQueue"
                                },
                                "scope": {
                                  "type": "string",
                                  "enum": [
                                    "all_sites",
                                    "sites_and_pages_you_look_after"
                                  ]
                                },
                                "site_id": {
                                  "type": "integer",
                                  "nullable": true
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "page": {
                        "id": 812,
                        "title": "News & Updates",
                        "display_title": "News & Updates",
                        "slug": "news-updates",
                        "page_type": "standard",
                        "status": "published",
                        "published_at": "2026-04-02T10:00:00Z",
                        "site": {
                          "id": 42,
                          "name": "Customer Success",
                          "display_name": "Customer Success"
                        },
                        "workflow_status": null
                      },
                      "site": {
                        "id": 42,
                        "name": "Customer Success",
                        "display_name": "Customer Success"
                      },
                      "kind": "expiring",
                      "kinds": [
                        "expiring"
                      ],
                      "detail": "Expires in 5 days",
                      "expires_at": "2026-10-27T17:00:00Z",
                      "last_reviewed_at": null,
                      "freshness_at": "2026-04-02T10:00:00Z",
                      "confirm_by": null,
                      "owner": {
                        "id": 7,
                        "name": "Dana Whitfield",
                        "active": true
                      },
                      "actions": {
                        "mark_accurate": false,
                        "extend_expiry": true,
                        "remind": false,
                        "review_refresh": false
                      }
                    }
                  ],
                  "meta": {
                    "total_count": 1,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false,
                    "queue": {
                      "key": "expiring",
                      "title": "Expiring in 30 days",
                      "cta_label": "Extend or archive"
                    },
                    "scope": "sites_and_pages_you_look_after",
                    "site_id": null
                  },
                  "total_count": 1
                }
              }
            }
          },
          "404": {
            "description": "`queue_not_found` — not one of the list keys."
          }
        }
      }
    },
    "/sites/pages/{page_id}/expiry_extension": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesCarePageId"
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Extend a page's expiry",
        "description": "\"Extend\" on the Expiring in 30 days list. Moves the page's expiry date `days` (default 90; a whole number from 1 to 365) past the later of its current date and now, so a lapsed page comes back. The web moves the same date in Page settings. The expiry warning is sent again before the new date. `still_expiring: false` means the row leaves the list. Each successful call extends again — send an `Idempotency-Key` so a retry does not.\n\nRefusals, in order: 404 `not_found` (also a page the caller cannot know exists); 409 `transfer_pending` (to the site's owners and editors); 403 `access_denied` / `not_allowed` (only the site's owners and editors and admins); 403 `site_trashed`; 422 `invalid_days`; 422 `no_expiry` (the page has no date to extend — set one in its Page settings); 422 `not_saved`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "default": 90
                  }
                }
              },
              "example": {
                "days": 90
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Extended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "The confirmation",
                      "in the caller's language.": null
                    },
                    "page": {
                      "$ref": "#/components/schemas/SitesCarePageRef"
                    },
                    "expiry": {
                      "type": "object",
                      "properties": {
                        "previous_expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "extended_by_days": {
                          "type": "integer"
                        }
                      }
                    },
                    "still_expiring": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "message": "Extended “News & Updates” by 90 days.",
                  "page": {
                    "id": 812,
                    "title": "News & Updates",
                    "site_id": 42
                  },
                  "expiry": {
                    "previous_expires_at": "2026-10-27T17:00:00Z",
                    "expires_at": "2027-01-25T17:00:00Z",
                    "extended_by_days": 90
                  },
                  "still_expiring": false
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/SitesCareRefused"
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "$ref": "#/components/responses/SitesCareTransferPending"
          },
          "422": {
            "description": "`invalid_days` (not a whole number from 1 to 365), `no_expiry` or `not_saved` (the reason is in `message`)."
          }
        }
      }
    },
    "/sites/pages/{page_id}/read_confirmation_reminders": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesCarePageId"
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Remind the readers who have not confirmed a must-read page",
        "description": "\"Send reminders\" on the Required reading behind list — the web roster's \"Remind the rest\". Reminds, in the app, everyone the live version asked who has not confirmed, is still an active member and can still open the page — except anyone reminded in the last `interval_hours`, and never the sender. `status`: `queued` (reminders are on their way to `reminding` people; `skipped` were reminded recently), `nobody` (everyone who can still open it confirmed), `all_recent` (everyone due was reminded recently), `sample` (sample pages never send), `withheld` (the group is too small to count — no numbers are given, and whoever owes it is still reminded). The page stays on the list until its readers confirm.\n\nRefusals: 404 `not_found` (also a page the caller cannot know exists); 409 `transfer_pending` (to the site's owners and editors); 403 `access_denied` / `not_allowed` (only the site's owners and editors and admins); 422 `not_asking` (the page does not ask readers to confirm).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sent, or why nobody was reminded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "page": {
                      "$ref": "#/components/schemas/SitesCarePageRef"
                    },
                    "reminder": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "nobody",
                            "all_recent",
                            "sample",
                            "withheld"
                          ]
                        },
                        "reminding": {
                          "type": "integer",
                          "nullable": true
                        },
                        "skipped": {
                          "type": "integer",
                          "nullable": true
                        },
                        "interval_hours": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "message": "Reminding 4 people who haven't confirmed yet.",
                  "page": {
                    "id": 640,
                    "title": "Fire evacuation",
                    "site_id": 42
                  },
                  "reminder": {
                    "status": "queued",
                    "reminding": 4,
                    "skipped": 0,
                    "interval_hours": 12
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/SitesCareRefused"
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "$ref": "#/components/responses/SitesCareTransferPending"
          },
          "422": {
            "description": "`not_asking` — this page isn't asking readers to confirm they've read it."
          }
        }
      }
    },
    "/sites/pages/{page_id}/source_refresh": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesCarePageId"
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Review a page's source refresh",
        "description": "\"Review updates\" on the Linked policy, form or person changed list — the web's Review refresh screen. `kind` is `source` (a policy, form or person a section is bound to changed; `drift` lists them and the proposal removes the records that are gone) or `import_update` (the web page it was imported from changed and an AI-prepared update waits; `changes` lists what changed). Read-only: a proposal not yet saved (`staged: false`) is computed for display, never written. `can_publish` / `publish_refusal` answer for the page as it stands now; the gate is asked again when it publishes. `can_apply` and `can_publish` are false for a token without `write:sites`, which apply asks.\n\nRefusals: 404 `not_found` (also a page the caller cannot know exists); 409 `transfer_pending` (to the site's owners and editors); 403 `access_denied` / `not_allowed`; 403 `content_sync_disabled` (the company's self-maintaining content is off and no update the editor asked for is waiting).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The proposal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source_refresh": {
                      "$ref": "#/components/schemas/SitesSourceRefresh"
                    }
                  }
                },
                "example": {
                  "source_refresh": {
                    "page": {
                      "id": 905,
                      "title": "Team Directory",
                      "site_id": 42,
                      "status": "published"
                    },
                    "kind": "source",
                    "staged": true,
                    "has_content_change": true,
                    "changes": [],
                    "drift": [
                      {
                        "section_id": "s3",
                        "section_type": "people",
                        "section_name": "People",
                        "heading": "Our team",
                        "issue": "removed",
                        "source_label": "People",
                        "missing_records": 1
                      }
                    ],
                    "proposed_sections": [
                      {
                        "position": 1,
                        "type": "people",
                        "type_name": "People",
                        "heading": "Our team",
                        "excerpt": null,
                        "differs_from_draft": true
                      }
                    ],
                    "proposal_digest": "9f2c…e1",
                    "generated_at": "2026-10-21T02:10:00Z",
                    "can_apply": true,
                    "can_publish": true,
                    "publish_refusal": null
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/SitesCareRefused"
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "$ref": "#/components/responses/SitesCareTransferPending"
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Dismiss a page's source refresh",
        "description": "Keeps the page as it is. An import update adopts the source's new version, so the same change is not offered again; a source refresh clears the proposal and dismisses its open findings. `findings_cleared: false` — the proposal was dismissed, but its findings could not be cleared now; they are checked again on the next sync.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "page": {
                      "$ref": "#/components/schemas/SitesCarePageRef"
                    },
                    "findings_cleared": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "message": "Refresh dismissed. The page was left unchanged.",
                  "page": {
                    "id": 905,
                    "title": "Team Directory",
                    "site_id": 42
                  },
                  "findings_cleared": true
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/SitesCareRefused"
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "$ref": "#/components/responses/SitesCareTransferPending"
          },
          "500": {
            "description": "`failed` — the update could not be dismissed; nothing changed."
          }
        }
      }
    },
    "/sites/pages/{page_id}/source_refresh/apply": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesCarePageId"
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Apply a page's source refresh, optionally publishing it",
        "description": "Writes the proposal into the page's DRAFT — the live page changes only when it is published. A source proposal not saved yet is prepared first (what the review showed). With `publish: true` the draft is then published through the same gate as the page's Publish button: a page that needs approval stays applied in its draft, with `published: false` and the reason in `publish_refusal`. Without `publish`, `published` is null. A publish that fails after the draft was written still answers 200, with `published: false` and the reason. Retries with the same `Idempotency-Key` replay the first answer.\n\nRefusals: 404 `not_found` (also a page the caller cannot know exists); 409 `transfer_pending` (to the site's owners and editors); 403 `access_denied` / `not_allowed` / `content_sync_disabled`; 422 `not_published` (a source refresh needs a published page); 422 `nothing_to_apply`; 409 `stale` (the page changed after the proposal was prepared — nothing was overwritten).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "publish": {
                    "type": "boolean",
                    "default": false
                  },
                  "proposal_digest": {
                    "type": "string",
                    "description": "The review's `proposal_digest`. When sent, a proposal that changed since the review is refused 409 `stale` instead of being applied."
                  }
                }
              },
              "example": {
                "publish": true,
                "proposal_digest": "9f2c…e1"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "page": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SitesCarePageRef"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "status": {
                              "type": "string"
                            }
                          }
                        }
                      ]
                    },
                    "published": {
                      "type": "boolean",
                      "nullable": true
                    },
                    "publish_refusal": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "message": "Update applied and published.",
                  "page": {
                    "id": 905,
                    "title": "Team Directory",
                    "site_id": 42,
                    "status": "published"
                  },
                  "published": true,
                  "publish_refusal": null
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/SitesCareRefused"
          },
          "404": {
            "description": "`not_found`."
          },
          "409": {
            "description": "`stale` — the page changed after this refresh was prepared, or the proposal no longer matches the `proposal_digest` sent; nothing was overwritten — review it again. `transfer_pending` — the page or its site is still being imported."
          },
          "500": {
            "description": "`failed` — the refresh could not be applied; the page draft was left unchanged."
          },
          "422": {
            "description": "`not_published` or `nothing_to_apply` (the reason is in `message`)."
          }
        }
      }
    },
    "/sites/pages/{id}/archive": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The page's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Archive a page",
        "description": "The web's \"Archive page\" (the page's ⋮ menu, the editor's page menu, the Pages list's bulk bar). The page stops being visible to readers but stays on the site for its editors, who can publish it again; a pending approval is ended. The page's author is told who archived it. Offered where `page.viewer.can_archive` is true: the site's owners and editors and company admins / Sites app-admins, on a page not already archived.\n\nRefusals, in order: a page outside the caller's company is 404; a page the caller can neither open nor archive is 403 `access_denied` (nothing about it is said); a page or site still being imported is 409 `transfer_pending`; anyone but the site's owners and editors and admins is 403 `not_allowed`; an archived page is 422 `already_archived` (and its author is not told twice); an archive the page itself refuses is 422 `not_removed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesPageArchived"
                },
                "example": {
                  "message": "Page archived",
                  "page": {
                    "id": 1617,
                    "title": "Holiday rota",
                    "site_id": 42,
                    "status": "archived"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`access_denied` — the caller can neither open this page nor archive it; `not_allowed` — only the site's owners and editors and admins can archive its pages; or the token lacks `destructive:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "You need editor access to modify pages in this site."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "description": "`transfer_pending` — the page or its site is still being imported."
          },
          "422": {
            "description": "`already_archived` — the page is already archived; `not_removed` — the page could not be archived (the reason is in `message`).",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "already_archived",
                    "message": "This page is already archived."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/sites/{id}/archive": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesSiteId"
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Archive a site",
        "description": "The web's \"Archive\" (the site's ⋮ menu). The site leaves All sites and its pages stop being available to readers; the site, its pages, members and followers are kept, and an owner or editor can publish it again. Its members and followers are told who archived it. Offered where the site's `can_archive` is true: the site's owners and editors and company admins / Sites app-admins, on a site not already archived.\n\nRefusals, in order: a site outside the caller's company is 404; a site the caller can neither open nor archive is 404 `not_found` too (nothing about it is said); a site still being imported is 409 `transfer_pending`; anyone but the site's owners and editors and admins is 403 `not_allowed`; a site in Trash is 409 `in_trash` (restore it first); an archived site is 422 `already_archived` (and nobody is told twice); an archive the site itself refuses is 422 `not_removed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesSiteArchived"
                },
                "example": {
                  "message": "Site archived",
                  "site": {
                    "id": 42,
                    "name": "Benefits 2025",
                    "status": "archived",
                    "archived_at": "2026-10-22T09:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`not_allowed` — only the site's owners and editors and admins can archive it; `insufficient_permissions` — the token lacks `destructive:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "You need editor access to modify this site."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no site with that id in the caller's company, or one the caller can neither open nor archive."
          },
          "409": {
            "description": "`transfer_pending` — the site is still being imported; `in_trash` — the site is in Trash: restore it from All sites › ⋮ › Trash first.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "in_trash",
                    "message": "\"Benefits 2025\" is in Trash. Restore it from All sites › ⋮ › Trash first."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`already_archived` — the site is already archived; `not_removed` — the site could not be archived (the reason is in `message`).",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "already_archived",
                    "message": "This site is already archived."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/sites/{id}/trash": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SitesSiteId"
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Move a site to Trash",
        "description": "The web's \"Move to Trash\" (the site's ⋮ menu). Readers and members lose access to the site and every page in it at once. Nothing is deleted: its pages, members, followers and bookmarks stay, and the site comes back — as a draft — when restored from All sites › ⋮ › Trash on the web, the only place a site can be deleted permanently. `status` keeps the value it had (Trash is not a status). Its members and followers are told who moved it; its owners, that they can restore it. Offered where the site's `can_trash` is true: the site's owners and company admins / Sites app-admins.\n\nRefusals, in order: a site outside the caller's company is 404; a site the caller can neither open nor move to Trash is 404 `not_found` too — which includes a site already in Trash, for anyone but an admin; a site still being imported is 409 `transfer_pending`; anyone but the site's owners and admins is 403 `not_allowed`; a site already in Trash is 422 `already_trashed` (admins); a move the site itself refuses is 422 `not_removed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Moved to Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesSiteTrashed"
                },
                "example": {
                  "message": "\"Benefits 2025\" moved to Trash. You can restore it from All sites › ⋮ › Trash.",
                  "site": {
                    "id": 42,
                    "name": "Benefits 2025",
                    "status": "published",
                    "trashed_at": "2026-10-22T09:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`not_allowed` — only the site's owners and admins can move it to Trash; `insufficient_permissions` — the token lacks `destructive:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "Only site owners can perform this action."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no site with that id in the caller's company, or one the caller can neither open nor move to Trash (a site already in Trash, for anyone but an admin)."
          },
          "409": {
            "description": "`transfer_pending` — the site is still being imported."
          },
          "422": {
            "description": "`already_trashed` — the site is already in Trash; `not_removed` — the site could not be moved (the reason is in `message`).",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "already_trashed",
                    "message": "\"Benefits 2025\" is already in Trash."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/sites/{id}/publish": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The site's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Publish a site",
        "description": "The web's \"Publish site\" (a draft site's button under Follow, and the site's Manage ▾ › Publish). The site opens at once to the people its audience admits — everyone in the company when it is for everyone, only the people its audience rules name when it is limited, and only its members and admins while it has no audience yet. Its draft pages stay drafts. To take it down later, archive it (`POST /sites/sites/{id}/archive`). An archived site, or one restored from Trash, published again tells its members and followers it is back. Offered where the site's `can_publish` is true; the draft prompt is `is_draft && can_publish`.\n\nRefusals, in order: a site outside the caller's company is 404; a site the caller can neither open nor publish is 404 `not_found` too (nothing about it is said); a site still being imported is 409 `transfer_pending`; anyone but the site's owners and editors and admins is 403 `not_allowed`; a site in Trash is 409 `in_trash` (restore it first); a published site is 422 `already_published`; a site whose imported audience a Sites admin has not yet chosen in Export & Import is 409 `audience_held`; an archived site published again while the company is at its site limit (Sites settings › Limits › Max sites) is 422 `over_site_limit` — an archived site holds no place against the limit; a publish the site itself refuses is 422 `not_published`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesSitePublished"
                },
                "example": {
                  "message": "Site published",
                  "site": {
                    "id": 42,
                    "name": "Q3 Launch — Lighthouse",
                    "status": "published",
                    "is_draft": false,
                    "published_at": "2026-10-22T09:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`not_allowed` — only the site's owners and editors and admins can publish it; `insufficient_permissions` — the token lacks `write:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "You need editor access to modify this site."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no site with that id in the caller's company, or one the caller can neither open nor publish."
          },
          "409": {
            "description": "`transfer_pending` — the site is still being imported; `in_trash` — the site is in Trash: restore it from All sites › ⋮ › Trash first; `audience_held` — a Sites admin must choose the site's imported audience in Export & Import before it can be published.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "in_trash",
                    "message": "\"Q3 Launch — Lighthouse\" is in Trash. Restore it from All sites › ⋮ › Trash first."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`already_published` — the site is already published; `over_site_limit` — the site is archived and the company is at its site limit: archive another site or raise the limit first; `not_published` — the site could not be published (the reason is in `message`).",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "already_published",
                    "message": "This site is already published."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/sites": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List the sites the caller can open",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "A site category of this tenant (`site_type`). An unknown value is 422 `invalid_type`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "A site status. Without it the list is the one All sites shows: live sites, never an archived one, and a draft only to its members (and Sites admins). `status=archived` lists the archived sites the caller may open. An unknown value is 422 `invalid_status`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "top_level",
            "in": "query",
            "required": false,
            "description": "`true` — only sites that sit under no other site (All sites' list).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "parent_site_id",
            "in": "query",
            "required": false,
            "description": "Only the sites directly under this site.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "yours",
            "in": "query",
            "required": false,
            "description": "`true` — \"Your sites\": the ones the caller is a member of (any role), follows, or that are For them. Never an archived site.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "grouped",
            "in": "query",
            "required": false,
            "description": "`true` — the rows in All sites' order: by group, A to Z inside each (instead of most recently updated first), with `meta.groups` counting every group over the whole filtered set, so each section can be headed with its count on any page. Use with `top_level=true` for the All sites screen. Combines with the other filters.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sites, most recently updated first — or, with `grouped=true`, by group then A to Z",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesSite"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "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"
                                }
                              }
                            },
                            {
                              "type": "object",
                              "properties": {
                                "groups": {
                                  "type": "array",
                                  "description": "Only with `grouped=true`: every group in display order, counted over the whole filtered set (not only this page). Empty groups are never listed.",
                                  "items": {
                                    "$ref": "#/components/schemas/SitesDirectoryGroup"
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "id": 12,
                      "name": "Customer Success",
                      "display_name": "Customer Success",
                      "site_type": "department",
                      "status": "published",
                      "pages_count": 14,
                      "member_role": "owner",
                      "is_published": true,
                      "is_draft": false,
                      "following": false,
                      "group": {
                        "key": "departments",
                        "title": "Departments"
                      },
                      "chip": "Member",
                      "sites_inside": 2,
                      "last_updated_at": "2026-10-16T14:05:00Z",
                      "can_edit": true,
                      "can_archive": true,
                      "can_trash": true,
                      "can_publish": false,
                      "restricted": false
                    }
                  ],
                  "meta": {
                    "total_count": 9,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false,
                    "groups": [
                      {
                        "key": "departments",
                        "title": "Departments",
                        "count": 3
                      },
                      {
                        "key": "category-company",
                        "title": "Company-wide",
                        "count": 2
                      },
                      {
                        "key": "category-knowledge_base",
                        "title": "Knowledge base",
                        "count": 2
                      },
                      {
                        "key": "category-team",
                        "title": "Team",
                        "count": 2
                      }
                    ]
                  },
                  "total_count": 9
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "The Sites app is not enabled or not accessible to the caller."
          },
          "422": {
            "description": "`invalid_type` or `invalid_status`."
          }
        }
      }
    },
    "/sites/sites/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Read one site",
        "description": "The site with its parent (when the caller may open it), the sites under it, who it is For, and its pages — at most 100, audience- and status-filtered for the caller; `pages_truncated` says when there are more (page through `GET /sites/pages?site_id=`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The site",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "site": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SitesSite"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "parent_site": {
                              "type": "object",
                              "nullable": true,
                              "description": "The site the back button names — the parent, when the caller may open it. Null for a top-level site, or a parent the caller cannot open (`parent_site_id` still says which it is).",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "display_name": {
                                  "type": "string",
                                  "description": "The name in the caller's language; the original when there is no translation."
                                }
                              }
                            },
                            "child_sites": {
                              "type": "array",
                              "maxItems": 100,
                              "description": "The published sites directly under this one that the caller may open, by name (a Sites app admin sees every one not archived, drafts included) — what each \"Sites in <name>\" card says.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "display_name": {
                                    "type": "string",
                                    "description": "The name in the caller's language; the original when there is no translation."
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "The site's status — a card badges anything but `published` (a Sites admin sees drafts)."
                                  },
                                  "sites_inside": {
                                    "type": "integer",
                                    "description": "The sites beneath it at any depth that the caller may open."
                                  },
                                  "owner": {
                                    "type": "object",
                                    "nullable": true,
                                    "description": "Its first active owner (who keeps it); null when it has none.",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  },
                                  "last_updated_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "description": "When a reader last saw it change — the later of the site row and its newest page change the caller can read."
                                  }
                                }
                              }
                            },
                            "owners": {
                              "type": "array",
                              "maxItems": 4,
                              "description": "The site's active owners, first four by when they joined (the Site info card); `owners_count` says how many there are in all.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              }
                            },
                            "owners_count": {
                              "type": "integer",
                              "description": "Every active owner of the site (\"+N more\" past `owners`)."
                            },
                            "followers_count": {
                              "type": "integer",
                              "description": "How many people follow the site."
                            },
                            "for_description": {
                              "type": "string",
                              "description": "Who the site is For, in words — present only when `for_criteria` names anyone.",
                              "example": "Bakery (and departments under it)"
                            },
                            "pages_truncated": {
                              "type": "boolean"
                            },
                            "pages": {
                              "type": "array",
                              "maxItems": 100,
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "title": {
                                    "type": "string"
                                  },
                                  "slug": {
                                    "type": "string"
                                  },
                                  "page_type": {
                                    "type": "string"
                                  },
                                  "status": {
                                    "type": "string"
                                  },
                                  "position": {
                                    "type": "integer",
                                    "nullable": true
                                  },
                                  "published_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "nullable": true
                                  },
                                  "updated_at": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "workflow_status": {
                                    "$ref": "#/components/schemas/SitesPageWorkflowStatus"
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such site, or one the caller cannot open (both answer alike, so ids cannot be walked to map hidden sites)."
          }
        }
      }
    },
    "/sites/sites/{site_id}/members": {
      "parameters": [
        {
          "name": "site_id",
          "in": "path",
          "required": true,
          "description": "The site's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List a site's members (Owners, Editors, Readers)",
        "description": "The web's Members page, in its order: owners, then editors, then viewers. It is paged like the web (50 a page by default, `per_page` up to 100). Each page's rows are dealt into three sections: `owners`, `editors` and `readers` (viewer memberships). Every section's `count` covers the whole site, so a client can head each section correctly on any page. A later page's `items` are appended to the section they belong to.\n\nThe `readers` section also carries the site's `audience`: who can read the site without a membership. Its `rules` name people, departments and groups, so they are given only to someone who manages the site's members (its owners and admins), as the web's Audience card is. `added_by` (who granted the membership) follows the same rule.\n\n`can_remove` at the top level says whether the caller may remove members (a site owner or a Sites admin, with a token holding `destructive:sites`). On a row, it says whether the caller can remove that member: for a manager, every member but the site's last owner; for anyone else, their own row (leaving the site), unless they are its last owner.\n\nSection `title`s, `role_label` and the audience's words come in the language the client asks for (`locale`, else `Accept-Language`, else the person's own language).\n\nRefusals: a site outside the caller's company, or one the caller may not open, is 404 `not_found`, as for an unknown id.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page of the roster, in sections.",
            "headers": {
              "X-Total-Count": {
                "schema": {
                  "type": "integer"
                },
                "description": "Members on the site."
              },
              "X-Total-Pages": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesMemberRoster"
                },
                "example": {
                  "site": {
                    "id": 42,
                    "name": "Customer Success",
                    "display_name": "Customer Success"
                  },
                  "can_remove": true,
                  "sections": [
                    {
                      "key": "owners",
                      "title": "Owners",
                      "role": "owner",
                      "count": 1,
                      "items": [
                        {
                          "id": 311,
                          "role": "owner",
                          "role_label": "Owner",
                          "user": {
                            "id": 7,
                            "name": "Olga Owner",
                            "avatar_url": "https://officechat.workforce.mangoapps.com/rails/active_storage/representations/…"
                          },
                          "is_current_user": true,
                          "can_remove": false,
                          "added_by": {
                            "id": 2,
                            "name": "Nina Admin"
                          }
                        }
                      ]
                    },
                    {
                      "key": "editors",
                      "title": "Editors",
                      "role": "editor",
                      "count": 2,
                      "items": [
                        {
                          "id": 312,
                          "role": "editor",
                          "role_label": "Editor",
                          "user": {
                            "id": 9,
                            "name": "Dana Editor",
                            "avatar_url": "https://ui-avatars.com/api/?name=Dana+Editor"
                          },
                          "is_current_user": false,
                          "can_remove": true,
                          "added_by": {
                            "id": 7,
                            "name": "Olga Owner"
                          }
                        }
                      ]
                    },
                    {
                      "key": "readers",
                      "title": "Readers",
                      "role": "viewer",
                      "count": 0,
                      "items": [],
                      "audience": {
                        "restricted": true,
                        "title": "Only specific people",
                        "text": "Only this audience, the site's members and admins can open the site.",
                        "rules": [
                          "Department: Finance",
                          "Location: Downtown"
                        ]
                      }
                    }
                  ],
                  "total_count": 3,
                  "meta": {
                    "total_count": 3,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 50,
                    "has_next_page": false,
                    "has_prev_page": false
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`insufficient_permissions` — the token lacks `read:sites`; `app_disabled` — Sites is not enabled for the company; `app_access_denied` — the caller has no access to the Sites app."
          },
          "404": {
            "description": "`not_found` — no site with that id in the caller's company, or one the caller may not open."
          }
        }
      }
    },
    "/sites/sites/{site_id}/members/{user_id}": {
      "parameters": [
        {
          "name": "site_id",
          "in": "path",
          "required": true,
          "description": "The site's id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "user_id",
          "in": "path",
          "required": true,
          "description": "The member's user id (`items[].user.id` in the roster).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Remove a member from a site",
        "description": "The web's Remove on the Members page. The person loses their role on the site. Any pages they owned on it pass to the site's first remaining active owner, else editor; with no such person, those pages are left without an owner. The person is told. They can still read the site if it is published and its audience admits them. Offered where the row's `can_remove` is true: the site's owners and company admins / Sites app-admins can remove anyone except the site's last owner, and any member can remove themselves (leave the site) unless they are its last owner. Removing yourself answers `left_site: true`.\n\nRefusals, in order: a site outside the caller's company is 404 `not_found`, and so is a site the caller can neither open nor manage; a site still being imported is 409 `transfer_pending` (admins included); anyone but the site's owners and admins, removing someone other than themselves, is 403 `not_allowed` (before the person is looked up); an unknown person is 404 `user_not_found`; a person who is not a member is 404 `not_a_member`; the site's last owner is 409 `last_owner`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesMemberRemoved"
                },
                "example": {
                  "message": "Member removed. The 1 page they owned here now belongs to Olga Owner.",
                  "removed": {
                    "user": {
                      "id": 9,
                      "name": "Dana Editor"
                    },
                    "role": "editor"
                  },
                  "left_site": false,
                  "pages_handed_on": {
                    "count": 1,
                    "new_owner": {
                      "id": 7,
                      "name": "Olga Owner"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`not_allowed` — only the site's owners and admins can remove other members; `insufficient_permissions` — the token lacks `destructive:sites`; `app_disabled` — Sites is not enabled for the company; `app_access_denied` — the caller has no access to the Sites app.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "Only site owners can perform this action."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no site with that id in the caller's company, or one the caller can neither open nor manage; `user_not_found` — no such person in the company; `not_a_member` — the person has no role on this site."
          },
          "409": {
            "description": "`last_owner` — a site must keep at least one owner; `transfer_pending` — the site is still being imported.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "last_owner",
                    "message": "A site must keep at least one owner."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/pages": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List the pages the caller can read",
        "description": "Pages of the sites the caller can open, audience-filtered for the caller; drafts and archived pages only where the caller may see them (their own, a review assigned to them, or — with `site_id` — a site they edit). Ordered as the site orders them.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "required": false,
            "description": "Only this site's pages. A site the caller cannot open is 404 `not_found`.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "A page status. An unknown value is 422 `invalid_status`.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published",
                "archived"
              ]
            }
          },
          {
            "name": "page_type",
            "in": "query",
            "required": false,
            "description": "A page type. An unknown value is 422 `invalid_page_type`.",
            "schema": {
              "type": "string",
              "enum": [
                "content",
                "news",
                "wiki",
                "announcement",
                "landing"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pages",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesPage"
                          }
                        },
                        "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"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "403": {
            "description": "The Sites app is not enabled or not accessible to the caller."
          },
          "404": {
            "description": "`not_found` — `site_id` names a site the caller cannot open."
          },
          "422": {
            "description": "`invalid_status` or `invalid_page_type`."
          }
        }
      }
    },
    "/sites/pages/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Read one page",
        "description": "The page with its body. A reader gets the published body cut to the sections their audience may read, in their language when a fresh, fully translated version exists (`?locale=` asks for one; `original` for the original). An editor of the site gets the unpublished DRAFT body, in the original language, and `ai_assisted` then describes that draft. Opening a page here counts as a read (Page Analytics, Recently read) — except an editor's look at a draft. Around the body it carries what the page's own screen shows (`SitesPageDetail`): reactions, the live version, \"updated since you last read\", the Approval panel, Going stale and the rest, each for the people the web page shows it to.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "A translation's language code, or `original`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "page": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SitesPage"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "sections": {
                              "type": "array",
                              "description": "The body the caller may read — the translation's when one is served, the draft for an editor.",
                              "items": {
                                "type": "object",
                                "additionalProperties": true
                              }
                            },
                            "content_text": {
                              "type": "string",
                              "nullable": true,
                              "description": "The same body as plain text."
                            },
                            "audience_description": {
                              "type": "string",
                              "description": "Who the page is for, in words — editors only."
                            },
                            "locale": {
                              "type": "string",
                              "description": "The language served — `original` for the original."
                            },
                            "locale_auto_selected": {
                              "type": "boolean",
                              "description": "True when the caller's own language was picked for them rather than asked for."
                            },
                            "available_locales": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "code": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "status": {
                                    "type": "string"
                                  },
                                  "verified": {
                                    "type": "boolean"
                                  },
                                  "stale": {
                                    "type": "boolean"
                                  },
                                  "sections_translated": {
                                    "type": "boolean"
                                  }
                                }
                              }
                            },
                            "read_confirmation": {
                              "type": "object",
                              "description": "Whether the live version of a page asks THIS caller to confirm reading it. `{ requested: false }` when it does not.",
                              "properties": {
                                "requested": {
                                  "type": "boolean"
                                },
                                "acknowledged": {
                                  "type": "boolean"
                                },
                                "acknowledged_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "version_number": {
                                  "type": "integer"
                                },
                                "due_on": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true
                                },
                                "overdue": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        },
                        {
                          "$ref": "#/components/schemas/SitesPageDetail"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only to a caller who can already see the page exists — an editor of the site, a reader of a page that was once live (archived, expired, unpublished), or the page's assigned reviewer outside its audience: `access_denied` or `not_published`, with the page's own reason."
          },
          "404": {
            "description": "`not_found` — the id does not exist, OR the caller may not know the page exists (it is on a site they cannot open, or never published to them). Both answer identically, so ids cannot be walked to map hidden pages."
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Move a page to Trash",
        "description": "The web's \"Move to Trash\" (the page's ⋮ menu, the editor's page menu, the Pages list's bulk bar). The page leaves the site and goes to the site's Manage › Pages › Trash, where its owners and editors can restore it as a draft for 90 days (`trash.restorable_until`). Comments, translations, view history, watchers, bookmarks and past versions are deleted for good, apart from any version that asked readers to confirm they had read the page (`trash.asked_to_confirm`), which comes back with it. The page's author is told who moved it. Offered where `page.viewer.can_delete` is true: the site's owners and company admins / Sites app-admins.\n\nRefusals, in order: a page outside the caller's company (or already in Trash) is 404; a page the caller can neither open nor move to Trash is 403 `access_denied` (nothing about it is said) — or 404 `not_found` when the caller cannot know the page exists; a page or site still being imported is 409 `transfer_pending`; anyone but the site's owners and admins is 403 `not_allowed`; a delete the page itself refuses is 422 `not_removed`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Moved to Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "The web's confirmation, in the caller's language."
                    },
                    "page": {
                      "$ref": "#/components/schemas/SitesRemovedPage"
                    },
                    "trash": {
                      "type": "object",
                      "properties": {
                        "restorable_until": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Until when the site's owners and editors can restore it (as a draft) from the site's Trash."
                        },
                        "restore_window_days": {
                          "type": "integer",
                          "description": "How long a page stays restorable in Trash."
                        },
                        "asked_to_confirm": {
                          "type": "boolean",
                          "description": "The page had versions that asked readers to confirm they had read it; those come back with it on restore, with the record of who confirmed."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "message": "Page moved to Trash. You can restore it from this site's Manage › Pages › Trash for 90 days.",
                  "page": {
                    "id": 1617,
                    "title": "Refund policy",
                    "site_id": 42
                  },
                  "trash": {
                    "restorable_until": "2027-01-19T09:30:00Z",
                    "restore_window_days": 90,
                    "asked_to_confirm": false
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`access_denied` — the caller can neither open this page nor move it to Trash, but can see it exists (otherwise 404 `not_found`); `not_allowed` — only the site's owners and admins can move its pages to Trash; or the token lacks `destructive:sites`.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "not_allowed",
                    "message": "Only site owners can move pages to Trash."
                  }
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "description": "`transfer_pending` — the page or its site is still being imported."
          },
          "422": {
            "description": "`not_removed` — the page could not be moved to Trash (the reason is in `message`)."
          }
        }
      }
    },
    "/sites/search": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Search the sites and pages the caller can read",
        "description": "The Sites search the phone and the desktop show, as data: the sites a\nquery finds (`sites`, first page only) and the pages it finds\n(`items`, paged). One query finds the same sites and pages, in the same\norder, as the phone's Sites search.\n\n**Sites** — the All sites search: live sites the caller may open, at\nany depth (published sites whose audience admits the caller, and sites\nthey are a member of or manage, drafts included; every live site for a\nSites admin; never an archived site). A site matches when `q` is in\nits name or description, or — with several words — when every word is\nin one or the other; a site whose name holds every word comes first,\nthen by name. At most 50 are listed; `meta.sites_more` is true when more\nmatched. Each is a site row of the caller's lists (its icon on its\n`tile_color`, `display_name`, `display_description`, `status` for a\nDraft badge, `restricted`) plus `pages_count` and `sites_inside` — the\nrow's \"3 pages · 2 sites inside\", counted as GET /sites/sites counts\nthem — and `parents`, where a nested site sits, root first, naming\nonly sites the caller may open. On page 2 and later `sites` is empty.\n\n**Pages** — the platform Search's Sites results: currently published\npages (not past their expiry) on sites the caller can open, filtered\nto the page's audience for the caller. A page matches on its title\n(closely, in any word order), or when `q` — or a quoted phrase in it,\nor one of the company's search aliases for it — appears in its body\ntext or description. A term of three or fewer letters matches only at\nthe start of a word. Best match first.\n\nEach page carries an `excerpt`: the sentence around the first match in\nthe body text the caller may read (or the description), with \"…\" where\nit was cut, and `matched_term`, the words as the page writes them. A\npage matched by its title alone has no `matched_term`; its `excerpt`\nis its description (up to 200 characters). A page whose only match is\nin a section hidden from the caller is left out. Its `site` is the site\nas every list draws it, so the row can show the site's icon on its\n`tile_color` beside the site's `display_name`.\n\nEvery branch answers the same shape — `sites` beside `items`, and the\npagination keys plus `query`, `results_count` and `sites_more` in\n`meta`:\n* `q` missing, blank, or not a single value (`q[]=…`): no search runs;\n  `items` and `sites` are empty, `meta.query` is `\"\"`,\n  `results_count` and `total_count` are 0.\n* Otherwise the results; `meta.query` echoes the trimmed `q` and\n  `results_count` equals `total_count` (pages).\n\nA first-page search is recorded for the Sites knowledge-gap report,\ncounted in pages.\n\n**Required Scopes:** `read:sites` (or `read:own_sites`). A token that\nnames scopes without either is refused with 403\n`insufficient_permissions` (\"Required scope: read:sites\").",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The search text. Surrounding whitespace is trimmed.",
            "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,
            "description": "Results per page; a value outside 1–100 is clamped to it.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching sites (first page) and matching pages, best match first",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items",
                        "sites"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "description": "The matching pages, paged.",
                          "items": {
                            "$ref": "#/components/schemas/SitesSearchResult"
                          }
                        },
                        "sites": {
                          "type": "array",
                          "description": "The matching sites, at most 50; empty on page 2 and later and when no search ran.",
                          "items": {
                            "$ref": "#/components/schemas/SitesSearchSite"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "required": [
                                "query",
                                "results_count",
                                "sites_more"
                              ],
                              "properties": {
                                "query": {
                                  "type": "string",
                                  "description": "The trimmed `q`; `\"\"` when no search ran."
                                },
                                "results_count": {
                                  "type": "integer",
                                  "description": "The same number as `total_count` (pages); 0 when no search ran."
                                },
                                "sites_more": {
                                  "type": "boolean",
                                  "description": "More sites matched than `sites` lists — type more words to narrow it."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "sites": [
                    {
                      "id": 41,
                      "name": "Engineering Team",
                      "display_name": "Engineering Team",
                      "slug": "engineering-team",
                      "site_type": "team",
                      "status": "published",
                      "icon": "fa-code",
                      "icon_color": null,
                      "tile_color": "#2e63b3",
                      "display_description": "Engineering team site — runbooks, on-call, and announcements.",
                      "restricted": false,
                      "pages_count": 3,
                      "sites_inside": 0,
                      "parents": []
                    }
                  ],
                  "items": [
                    {
                      "id": 812,
                      "title": "Service Catalogue",
                      "slug": "service-catalogue",
                      "page_type": "content",
                      "description": "What Engineering Team handles, what is self-serve, and how fast we answer.",
                      "excerpt": "What Engineering Team handles, what is self-serve, and how fast we answer.",
                      "matched_term": "Engineering",
                      "published_at": "2026-10-02T09:00:00Z",
                      "updated_at": "2026-10-02T09:00:00Z",
                      "site": {
                        "id": 41,
                        "name": "Engineering Team",
                        "display_name": "Engineering Team",
                        "slug": "engineering-team",
                        "site_type": "team",
                        "status": "published",
                        "icon": "fa-code",
                        "icon_color": null,
                        "tile_color": "#2e63b3"
                      },
                      "created_by": {
                        "id": 7,
                        "name": "Dana Whitfield"
                      }
                    }
                  ],
                  "meta": {
                    "query": "Engineering",
                    "results_count": 3,
                    "sites_more": false,
                    "current_page": 1,
                    "per_page": 20,
                    "total_count": 3,
                    "total_pages": 1,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "total_count": 3
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "* `insufficient_permissions` — the token lacks the scope (\"Required scope: read:sites\").\n* `app_disabled` — the Sites app is not enabled for the business.\n* `app_access_denied` — the caller does not have access to the Sites app.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Sites app is not configured on this installation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites/bookmarks": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List the caller's bookmarks",
        "description": "The caller's own bookmarks — of sites and of pages — most recently\nbookmarked first. Only bookmarks whose site or page the caller can\nstill open are listed: a bookmarked site moved to Trash, back to\ndraft, or whose membership or audience the caller lost is left out,\nand so is a page the caller can no longer read (its status, its\naudience, or its site). Such a bookmark is kept and reappears when\naccess returns.\n\n**Required Scopes:** `read:sites` (or `read:own_sites`). A token that\nnames scopes without either is refused with 403\n`insufficient_permissions` (\"Required scope: read:sites\").",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Bookmarks per page; a value outside 1–100 is clamped to it.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bookmarks, most recent first",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SitesBookmark"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "* `insufficient_permissions` — the token lacks the scope (\"Required scope: read:sites\").\n* `app_disabled` — the Sites app is not enabled for the business.\n* `app_access_denied` — the caller does not have access to the Sites app.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Sites app is not configured on this installation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Bookmark a site or a page",
        "description": "Idempotent: bookmarking something already bookmarked answers the existing bookmark (201), never removes it. Honours `Idempotency-Key`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bookmarkable_type",
                  "bookmarkable_id"
                ],
                "properties": {
                  "bookmarkable_type": {
                    "type": "string",
                    "enum": [
                      "Site",
                      "SitePage"
                    ]
                  },
                  "bookmarkable_id": {
                    "type": "integer",
                    "description": "A site the caller can open, or a page they can read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The bookmark",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bookmark": {
                      "$ref": "#/components/schemas/SitesBookmark"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token."
          },
          "403": {
            "description": "The token lacks `write:sites`, or the Sites app is not enabled or not accessible to the caller."
          },
          "404": {
            "description": "`not_found` — no site or page the caller can open has that id."
          },
          "409": {
            "description": "`bookmark_conflict` — the bookmark could not be written; try again."
          },
          "422": {
            "description": "`invalid_bookmarkable_type`, or a validation error."
          }
        }
      }
    },
    "/sites/bookmarks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The bookmark's id (not the site's or page's).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Remove one of the caller's bookmarks",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Bookmark removed"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token."
          },
          "403": {
            "description": "The token lacks `write:sites`, or the Sites app is not enabled or not accessible to the caller."
          },
          "404": {
            "description": "`not_found` — the caller has no bookmark with that id."
          },
          "422": {
            "description": "`delete_failed`."
          }
        }
      }
    },
    "/sites/dashboard": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "The caller's Sites Dashboard — decisions, care and Your sites",
        "description": "The three sections of the desktop Dashboard that need no period: the decisions waiting on the caller (approval requests to decide, their own requests sent back, approved pages they may publish, pages scheduled to publish), the problems Sites detects on the pages they look after, and the sites they own or edit with 30-day reads. A section with nothing in it is empty, never padded. Call `GET /sites/dashboard/performance` for the figures when `performance_available` is true.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The Dashboard",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dashboard": {
                      "type": "object",
                      "properties": {
                        "looks_after_pages": {
                          "type": "boolean",
                          "description": "Whether the caller looks after any page — a Sites admin, or an owner or editor of a site, or a page's owner. Care rows and the figures are only for them."
                        },
                        "analytics_enabled": {
                          "type": "boolean",
                          "description": "The tenant records page views (`enable_analytics`, on by default)."
                        },
                        "performance_available": {
                          "type": "boolean",
                          "description": "`looks_after_pages` and `analytics_enabled` — `GET /sites/dashboard/performance` has figures to give."
                        },
                        "decisions": {
                          "type": "object",
                          "description": "\"Decisions waiting on you\". Not drawn on the web while `cards` and `snoozed` are both empty.",
                          "properties": {
                            "cards": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/SitesDashboardDecisionCard"
                              }
                            },
                            "snoozed": {
                              "type": "array",
                              "description": "Cards the caller snoozed on the web; each comes back on its own when its set changes.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "key": {
                                    "type": "string",
                                    "example": "sites.reviews"
                                  },
                                  "title": {
                                    "type": "string",
                                    "example": "Pending approval"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "pages_needing_care": {
                          "type": "object",
                          "nullable": true,
                          "description": "\"Pages that need care\". Null when `looks_after_pages` is false. Empty `rows` means nothing needs care.",
                          "properties": {
                            "scope": {
                              "type": "string",
                              "enum": [
                                "all_sites",
                                "sites_and_pages_you_look_after"
                              ],
                              "description": "`all_sites` for a Sites admin; otherwise the sites the caller owns or edits and the pages they own."
                            },
                            "rows": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/SitesDashboardCareRow"
                              }
                            }
                          }
                        },
                        "your_sites": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer",
                              "description": "Every site the caller owns or edits."
                            },
                            "has_more": {
                              "type": "boolean",
                              "description": "`rows` holds the first 10 by name and `total` is larger (\"Showing 10 of N\")."
                            },
                            "reads_days": {
                              "type": "integer",
                              "example": 30,
                              "description": "The window `reads` covers — always 30 days, whatever period the figures use."
                            },
                            "rows": {
                              "type": "array",
                              "maxItems": 10,
                              "items": {
                                "$ref": "#/components/schemas/SitesDashboardSiteRow"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "dashboard": {
                    "looks_after_pages": true,
                    "analytics_enabled": true,
                    "performance_available": true,
                    "decisions": {
                      "cards": [
                        {
                          "key": "sites.reviews",
                          "title": "Pending approval",
                          "count": 1,
                          "count_label": null,
                          "tone": "info",
                          "icon": "fas fa-clipboard-check",
                          "lead": "Working Here",
                          "lead_page_id": 812,
                          "lead_site_id": 64,
                          "sub": "People & Culture · waiting 6 days",
                          "cta": {
                            "label": "Open approvals",
                            "list": "approvals",
                            "filter": "mine",
                            "web_path": "/apps/sites/reviews?filter=mine"
                          }
                        }
                      ],
                      "snoozed": []
                    },
                    "pages_needing_care": {
                      "scope": "sites_and_pages_you_look_after",
                      "rows": [
                        {
                          "key": "stale",
                          "title": "Going stale",
                          "count": 1,
                          "count_text": "1 page",
                          "detail": "Not published or checked in 180 days · oldest: Quick Start Guide, published 7 months ago",
                          "cta_label": "Review pages",
                          "queue": "stale"
                        }
                      ]
                    },
                    "your_sites": {
                      "total": 1,
                      "has_more": false,
                      "reads_days": 30,
                      "rows": [
                        {
                          "site": {
                            "id": 70,
                            "name": "Customer Success",
                            "display_name": "Customer Success",
                            "slug": "customer-success",
                            "site_type": "department",
                            "status": "published",
                            "is_published": true,
                            "icon": "fa-headset",
                            "icon_color": null,
                            "cover_image": null
                          },
                          "pages_count": 5,
                          "pages_needing_care": 2,
                          "reads": 1284,
                          "reads_previous": 1088,
                          "trend_percent": 18,
                          "last_published_at": "2026-10-09T10:12:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`dashboard_not_available` — the Dashboard has nothing for the caller (they do not administer, build, approve or own pages; send them to All sites); `insufficient_permissions` — the token lacks `read:sites`; `app_disabled` / `app_access_denied` — the Sites app is off for the business or not open to the caller.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "dashboard_not_available",
                    "message": "The Sites Dashboard is for people who look after, approve or build pages. Your sites are under All sites."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sites/dashboard/performance": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "How the caller's pages are doing, over 7, 30 or 90 days",
        "description": "The Dashboard's figures over the pages the caller looks after (every page for a Sites admin): reads with the change against the same number of days before, readers (counted, never named), the questions Ask AI pulled these pages into, the three most read pages, the three pages Ask AI used most, and the pages nobody read — or, for a Sites admin with the knowledge-gap report on, the searches that found nothing. `performance` is null for a caller who looks after no page (an approver who builds and owns nothing).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Days to cover — 7, 30 (the default) or 90. Any other value is 422 `invalid_period`.",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90
              ],
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The figures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "performance": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "period_days": {
                          "type": "integer",
                          "example": 30
                        },
                        "periods": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          },
                          "example": [
                            7,
                            30,
                            90
                          ]
                        },
                        "reads": {
                          "type": "integer",
                          "description": "Page opens in the period."
                        },
                        "reads_previous": {
                          "type": "integer",
                          "description": "Page opens in the same number of days before it."
                        },
                        "trend_percent": {
                          "type": "integer",
                          "nullable": true,
                          "description": "`reads` against `reads_previous`, in percent. Null when there is nothing earlier to compare with. At or above `trend_percent_cap` the web says \"Up from <reads_previous> in the <period_days> days before\" instead of a percentage."
                        },
                        "trend_percent_cap": {
                          "type": "integer",
                          "example": 1000
                        },
                        "readers": {
                          "type": "integer",
                          "description": "People who opened at least one of these pages."
                        },
                        "ai_questions": {
                          "type": "integer",
                          "description": "Questions Ask AI pulled these pages into."
                        },
                        "most_read": {
                          "type": "array",
                          "maxItems": 3,
                          "items": {
                            "type": "object",
                            "properties": {
                              "page": {
                                "$ref": "#/components/schemas/SitesDashboardPage"
                              },
                              "reads": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "ai_pages": {
                          "type": "array",
                          "maxItems": 3,
                          "items": {
                            "type": "object",
                            "properties": {
                              "page": {
                                "$ref": "#/components/schemas/SitesDashboardPage"
                              },
                              "questions": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "unread": {
                          "type": "object",
                          "nullable": true,
                          "description": "Published before the period and opened by nobody in it. Null when `search_gaps` is present.",
                          "properties": {
                            "count": {
                              "type": "integer"
                            },
                            "pages": {
                              "type": "array",
                              "maxItems": 3,
                              "items": {
                                "$ref": "#/components/schemas/SitesDashboardPage"
                              }
                            }
                          }
                        },
                        "search_gaps": {
                          "type": "object",
                          "nullable": true,
                          "description": "Sites admins with the knowledge-gap report on: the searches that found nothing, over the report's own window (`period_days`), whatever `period` asked for. Null for everyone else.",
                          "properties": {
                            "period_days": {
                              "type": "integer",
                              "example": 30
                            },
                            "sufficient_data": {
                              "type": "boolean",
                              "description": "False while there are too few searches to report on (\"Not enough searches to report on yet\")."
                            },
                            "queries": {
                              "type": "array",
                              "maxItems": 3,
                              "items": {
                                "type": "object",
                                "properties": {
                                  "query": {
                                    "type": "string",
                                    "example": "parking permit"
                                  },
                                  "searches": {
                                    "type": "integer",
                                    "example": 4
                                  },
                                  "now_finds": {
                                    "type": "integer",
                                    "description": "Pages those words find today — above 0, the gap has since been answered."
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "performance": {
                    "period_days": 30,
                    "periods": [
                      7,
                      30,
                      90
                    ],
                    "reads": 1284,
                    "reads_previous": 1088,
                    "trend_percent": 18,
                    "trend_percent_cap": 1000,
                    "readers": 96,
                    "ai_questions": 41,
                    "most_read": [
                      {
                        "page": {
                          "id": 812,
                          "title": "Team Directory",
                          "slug": "team-directory",
                          "site": {
                            "id": 70,
                            "name": "Customer Success",
                            "display_name": "Customer Success"
                          }
                        },
                        "reads": 412
                      }
                    ],
                    "ai_pages": [
                      {
                        "page": {
                          "id": 815,
                          "title": "How We Work",
                          "slug": "how-we-work",
                          "site": {
                            "id": 70,
                            "name": "Customer Success",
                            "display_name": "Customer Success"
                          }
                        },
                        "questions": 19
                      }
                    ],
                    "unread": {
                      "count": 1,
                      "pages": [
                        {
                          "id": 818,
                          "title": "Quick Start Guide",
                          "slug": "quick-start-guide",
                          "site": {
                            "id": 70,
                            "name": "Customer Success",
                            "display_name": "Customer Success"
                          }
                        }
                      ]
                    },
                    "search_gaps": null
                  }
                }
              }
            }
          },
          "403": {
            "description": "`dashboard_not_available` — as `GET /sites/dashboard`; `analytics_disabled` — the tenant does not record page views; `insufficient_permissions` — the token lacks `read:sites`."
          },
          "422": {
            "description": "`invalid_period` — `period` is not 7, 30 or 90."
          }
        }
      }
    },
    "/sites/approvals": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "The caller's Sites approval queue, by filter",
        "description": "Pages sent for approval, filtered as the desktop chips are: `waiting` (Waiting on you — requests the caller can decide, their own requests sent back to them, and their approved pages ready to publish), `all` (All pending — every request the caller may see except their own), `mine` (Assigned to me — what they can decide; for a Sites admin, the requests named for them), `sent` (Sent by me — their own requests still waiting, sent back, or approved and not live yet) and `ready` (Ready to publish — the part of Sent by me they can publish now).\n\nOrder: requests to decide oldest request first (the one holding someone up leads); Sent by me sent back first, then approved, then waiting, latest decision first; Waiting on you requests to decide, then sent back, then ready to publish.\n\n`filters` are the chips to draw, in order: Waiting on you and Ready to publish appear only while they hold something or are the selected filter. Each row is a request (`kind: request`) or the caller's own (`kind: sent`) and lists the `actions` the caller may take, in the order the web draws them. This endpoint lists; it decides nothing.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which chip. Omitted, it opens where the Sites navigation's Approvals item does: `waiting` while something waits on the caller, else `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "waiting",
                "all",
                "mine",
                "sent",
                "ready"
              ]
            }
          },
          {
            "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": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The filter's rows, the chips and their counts",
            "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": {
                        "$ref": "#/components/schemas/SitesApprovalRow"
                      }
                    },
                    "filter": {
                      "type": "string",
                      "enum": [
                        "waiting",
                        "all",
                        "mine",
                        "sent",
                        "ready"
                      ],
                      "description": "The filter these rows are — the one asked for, or the default."
                    },
                    "filters": {
                      "type": "array",
                      "description": "The chips to draw, in order.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "enum": [
                              "waiting",
                              "all",
                              "mine",
                              "sent",
                              "ready"
                            ]
                          },
                          "label": {
                            "type": "string",
                            "example": "Waiting on you"
                          },
                          "count": {
                            "type": "integer",
                            "description": "Every row the chip lists",
                            "over all pages.": null
                          },
                          "selected": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Every filter's count, shown as a chip or not.",
                      "properties": {
                        "waiting": {
                          "type": "integer"
                        },
                        "all": {
                          "type": "integer"
                        },
                        "mine": {
                          "type": "integer"
                        },
                        "sent": {
                          "type": "integer"
                        },
                        "ready": {
                          "type": "integer"
                        }
                      }
                    },
                    "waiting_label": {
                      "type": "string",
                      "description": "What the Waiting on you count is made of, for a screen reader.",
                      "example": "3 waiting on you: 1 to decide, 1 sent back to you, 1 ready to publish"
                    },
                    "empty_state": {
                      "nullable": true,
                      "description": "The selected filter's empty state, worded for that filter. Present only when the filter lists nothing at all (null past the last page of a filter that has rows).",
                      "type": "object",
                      "properties": {
                        "icon": {
                          "type": "string",
                          "example": "fas fa-clipboard-check"
                        },
                        "title": {
                          "type": "string",
                          "example": "No pages waiting for approval"
                        },
                        "text": {
                          "type": "string",
                          "example": "Everything sent for approval has been decided."
                        },
                        "cta": {
                          "nullable": true,
                          "type": "object",
                          "description": "A button that opens another filter.",
                          "properties": {
                            "label": {
                              "type": "string",
                              "example": "Open Sent by me"
                            },
                            "filter": {
                              "type": "string",
                              "enum": [
                                "waiting",
                                "all",
                                "mine",
                                "sent",
                                "ready"
                              ]
                            }
                          }
                        }
                      }
                    },
                    "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"
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 412,
                      "kind": "request",
                      "title": "How We Work",
                      "renames": null,
                      "page": {
                        "id": 412,
                        "title": "How We Work",
                        "display_title": "How We Work",
                        "slug": "how-we-work",
                        "page_type": "standard",
                        "status": "published",
                        "published_at": "2026-09-02T15:04:00Z",
                        "site": {
                          "id": 18,
                          "name": "Customer Success",
                          "display_name": "Customer Success",
                          "slug": "customer-success",
                          "site_type": "team",
                          "status": "published",
                          "icon": "fa-headset",
                          "icon_color": null,
                          "tile_color": "#0E7490"
                        }
                      },
                      "person": {
                        "id": 77,
                        "name": "Nina Patel",
                        "avatar_url": "https://officechat.workforce.mangoapps.com/rails/active_storage/representations/redirect/…"
                      },
                      "requested_by": {
                        "id": 77,
                        "name": "Nina Patel"
                      },
                      "approver": {
                        "id": 54,
                        "name": "Dana Whitfield"
                      },
                      "decided_by": null,
                      "review": {
                        "status": "pending_review",
                        "status_label": null,
                        "badge": "asked",
                        "wait_flag_after_days": 3,
                        "requested_at": "2026-10-16T09:12:00Z",
                        "reviewed_at": null,
                        "withdrawn_by_edit": false,
                        "approved_state": null,
                        "note": "Updated the escalation steps for the new on-call rota.",
                        "scheduled_publish_at": null,
                        "schedule_text": null,
                        "revision": "9f2c…"
                      },
                      "waiting_for": null,
                      "actions": [
                        "request_changes",
                        "reject",
                        "approve"
                      ],
                      "last_published_version": 4
                    }
                  ],
                  "filter": "waiting",
                  "filters": [
                    {
                      "key": "waiting",
                      "label": "Waiting on you",
                      "count": 1,
                      "selected": true
                    },
                    {
                      "key": "all",
                      "label": "All pending",
                      "count": 1,
                      "selected": false
                    },
                    {
                      "key": "mine",
                      "label": "Assigned to me",
                      "count": 1,
                      "selected": false
                    },
                    {
                      "key": "sent",
                      "label": "Sent by me",
                      "count": 1,
                      "selected": false
                    }
                  ],
                  "counts": {
                    "waiting": 1,
                    "all": 1,
                    "mine": 1,
                    "sent": 1,
                    "ready": 0
                  },
                  "waiting_label": "1 waiting on you: 1 to decide",
                  "empty_state": null,
                  "meta": {
                    "total_count": 1,
                    "total_pages": 1,
                    "current_page": 1,
                    "per_page": 20,
                    "has_next_page": false,
                    "has_prev_page": false
                  },
                  "total_count": 1
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "description": "`approvals_disabled` — page approvals are turned off for the tenant (the `enable_content_review` setting); `insufficient_permissions` — the token lacks `read:sites`; `app_disabled` / `app_access_denied` — the Sites app is off for the business or not open to the caller.",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "approvals_disabled",
                    "message": "Page approvals are turned off for your company."
                  }
                }
              }
            }
          },
          "422": {
            "description": "`invalid_filter` — `filter` is not one of waiting, all, mine, sent, ready."
          }
        }
      }
    },
    "/sites/approvals/{id}/approve": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Approve a page sent for approval",
        "description": "The Approvals row's Approve. The note is optional; approving without one keeps the request note. Once approved, the page can be published.\n\nWho may decide: the approver the page was sent to, or a company admin or Sites app-admin — never whoever asked for the approval, and nobody while the page's site is in the Trash. Asked before anything about the page's state, so a caller who may not decide learns nothing about it. `review_revision` is the row's `review.revision`: a request that changed after the caller loaded it (an edit, a new approver, someone else's decision) is refused, never decided. Whoever asked, and the page's author, are notified as on the web. Send an `Idempotency-Key` header to make a retry safe.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SitesApprovalPageId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SitesApprovalApproveRequest"
              },
              "example": {
                "review_revision": "9f2c…",
                "notes": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesApprovalDecision"
                },
                "example": {
                  "id": 412,
                  "decision": "approve",
                  "message": "Page approved.",
                  "title": "How We Work",
                  "requested_by": {
                    "id": 77,
                    "name": "Nina Patel"
                  },
                  "review": {
                    "status": "approved",
                    "status_label": "Approved",
                    "reviewed_at": "2026-10-19T10:02:00Z",
                    "decided_by": {
                      "id": 54,
                      "name": "Dana Whitfield"
                    },
                    "note": null
                  },
                  "waiting_count": 2,
                  "waiting_label": "2 waiting on you: 1 to decide, 1 ready to publish"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesApprovalRevisionMissing"
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "$ref": "#/components/responses/SitesApprovalDecisionForbidden"
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "$ref": "#/components/responses/SitesApprovalDecisionConflict"
          },
          "422": {
            "description": "`not_saved` — the page refused the change (the message says why)."
          }
        }
      }
    },
    "/sites/approvals/{id}/reject": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Reject a page sent for approval",
        "description": "The Approvals row's Reject — stop this version. A note saying why is required: the person who asked for approval is sent it.\n\nWho may decide: the approver the page was sent to, or a company admin or Sites app-admin — never whoever asked for the approval, and nobody while the page's site is in the Trash. Asked before anything about the page's state, so a caller who may not decide learns nothing about it. `review_revision` is the row's `review.revision`: a request that changed after the caller loaded it (an edit, a new approver, someone else's decision) is refused, never decided. Whoever asked, and the page's author, are notified as on the web. Send an `Idempotency-Key` header to make a retry safe.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SitesApprovalPageId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SitesApprovalNoteRequest"
              },
              "example": {
                "review_revision": "9f2c…",
                "notes": "We are retiring this policy."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesApprovalDecision"
                },
                "example": {
                  "id": 412,
                  "decision": "reject",
                  "message": "Page rejected.",
                  "title": "How We Work",
                  "requested_by": {
                    "id": 77,
                    "name": "Nina Patel"
                  },
                  "review": {
                    "status": "rejected",
                    "status_label": "Rejected",
                    "reviewed_at": "2026-10-19T10:02:00Z",
                    "decided_by": {
                      "id": 54,
                      "name": "Dana Whitfield"
                    },
                    "note": "We are retiring this policy."
                  },
                  "waiting_count": 2,
                  "waiting_label": "2 waiting on you: 1 to decide, 1 ready to publish"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesApprovalRevisionMissing"
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "$ref": "#/components/responses/SitesApprovalDecisionForbidden"
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "$ref": "#/components/responses/SitesApprovalDecisionConflict"
          },
          "422": {
            "description": "`note_required` — no note (or only spaces); `not_saved` — the page refused the change."
          }
        }
      }
    },
    "/sites/approvals/{id}/request_changes": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Request changes on a page sent for approval",
        "description": "The Approvals row's Request changes — send it back with what to fix. A note is required: the person who asked for approval is sent it.\n\nWho may decide: the approver the page was sent to, or a company admin or Sites app-admin — never whoever asked for the approval, and nobody while the page's site is in the Trash. Asked before anything about the page's state, so a caller who may not decide learns nothing about it. `review_revision` is the row's `review.revision`: a request that changed after the caller loaded it (an edit, a new approver, someone else's decision) is refused, never decided. Whoever asked, and the page's author, are notified as on the web. Send an `Idempotency-Key` header to make a retry safe.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SitesApprovalPageId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SitesApprovalNoteRequest"
              },
              "example": {
                "review_revision": "9f2c…",
                "notes": "Add the on-call number."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitesApprovalDecision"
                },
                "example": {
                  "id": 412,
                  "decision": "request_changes",
                  "message": "Changes requested.",
                  "title": "How We Work",
                  "requested_by": {
                    "id": 77,
                    "name": "Nina Patel"
                  },
                  "review": {
                    "status": "changes_requested",
                    "status_label": "Changes requested",
                    "reviewed_at": "2026-10-19T10:02:00Z",
                    "decided_by": {
                      "id": 54,
                      "name": "Dana Whitfield"
                    },
                    "note": "Add the on-call number."
                  },
                  "waiting_count": 2,
                  "waiting_label": "2 waiting on you: 1 to decide, 1 ready to publish"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SitesApprovalRevisionMissing"
          },
          "401": {
            "description": "`unauthorized` — no or an invalid token."
          },
          "403": {
            "$ref": "#/components/responses/SitesApprovalDecisionForbidden"
          },
          "404": {
            "description": "`not_found` — no page with that id in the caller's company."
          },
          "409": {
            "$ref": "#/components/responses/SitesApprovalDecisionConflict"
          },
          "422": {
            "description": "`note_required` — no note (or only spaces); `not_saved` — the page refused the change."
          }
        }
      }
    },
    "/countries": {
      "get": {
        "tags": [
          "Countries"
        ],
        "summary": "List the business's operating countries",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Countries, by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "countries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OperatingCountry"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "configured": {
                          "type": "boolean",
                          "description": "Whether the business chose its countries (false — `countries` is every country)."
                        },
                        "primary_country": {
                          "type": "string",
                          "nullable": true,
                          "example": "US"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          }
        }
      }
    },
    "/countries/all": {
      "get": {
        "tags": [
          "Countries"
        ],
        "summary": "List every available country (admin configuration)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Every country, with the business's enabled codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "countries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OperatingCountry"
                      }
                    },
                    "enabled_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "US",
                        "CA",
                        "DE"
                      ]
                    },
                    "primary_country": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          }
        }
      }
    },
    "/policy_hub/policies": {
      "get": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "List policies",
        "description": "The published policies the caller may read — a read-restricted policy is\nleft out for everyone outside its readers, its acknowledgers and HR —\nmost recently changed first (`updated_at` descending, then `id`\ndescending, so pages never skip or repeat a policy). Each entry is the\npolicy's summary; open `GET /policy_hub/policies/{id}` for its content,\nwho it applies to and the caller's acknowledgment state. Policy Hub\nmust be enabled for the business and accessible to the caller.\n\n**Required Scopes:** `read:policy_hub` (#7183) — the scope of every\nPolicy Hub read. A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:policy_hub\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only policies in this category (exact match). Repeat as `category[]=` to accept any of several. Empty means every category; an unknown category returns an empty list.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Words to look for. Each word (split on spaces) must appear, case-insensitively, in the policy's title or description. Empty means no search.",
            "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,
            "description": "Policies per page. Values above 100 are clamped to 100; an empty, zero, negative or non-numeric value uses the default.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of policies",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PolicyHubPolicySummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:policy_hub\" — the token lacks the scope), `feature_not_enabled` (Policy Hub is not enabled for the business) or `app_forbidden` (the caller has no access to Policy Hub).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policy_hub/handbooks": {
      "get": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "List handbooks",
        "description": "The published handbooks aimed at the caller — those for all employees,\nand those targeted at a department or location the caller belongs to\n(a location rollup includes the stores under it) — most recently\nchanged first (`updated_at` descending, then `id` descending). Each\nentry is the handbook's summary; open `GET /policy_hub/handbooks/{id}`\nfor its introduction and sections. Policy Hub must be enabled for the\nbusiness and accessible to the caller.\n\n**Required Scopes:** `read:policy_hub` (#7183) — the scope of every\nPolicy Hub read. A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:policy_hub\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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",
            "required": false,
            "description": "Handbooks per page. Values above 100 are clamped to 100; an empty, zero, negative or non-numeric value uses the default.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of handbooks",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PolicyHubHandbookSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:policy_hub\" — the token lacks the scope), `feature_not_enabled` (Policy Hub is not enabled for the business) or `app_forbidden` (the caller has no access to Policy Hub).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policy_hub/policies/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "Read one policy",
        "description": "A published policy the caller may read, with their own acknowledgment\nstate. Reading it records a view where the policy asks the caller for an\nacknowledgment. Policy Hub must be enabled for the business and\naccessible to the caller.\n\n**Required Scopes:** `read:policy_hub` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\") — the scope of every Policy Hub read\n(policies, handbooks, your acknowledgments). Acknowledging a policy\n(`POST /policy_hub/policies/{id}/acknowledge`) asks `write:notifications`\ninstead, the scope every acknowledgment asks (#5068). A token without the\nscope gets `403 insufficient_permissions` (\"Required scope: …\") before\nanything else is checked. The sign-in tokens hold both; a browser session\nis asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The policy, under a `policy` key and nothing else (`render_single`: no `unread_notification_count`, no `_meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "policy"
                  ],
                  "properties": {
                    "policy": {
                      "$ref": "#/components/schemas/PolicyHubPolicyDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (the token lacks `read:policy_hub`), `feature_not_enabled` or `app_forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no published policy the caller may read: \"The requested resource was not found.\" for an id that does not exist in the business or a read-restricted policy the caller may not read, \"Policy not found.\" for one that is not published (draft, archived, retired) and never asked the caller (they hold no acknowledgment row on it — otherwise `410`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`policy_withdrawn` — the policy is no longer published (a draft, archived or retired) and the caller holds an acknowledgment row on it, so it once asked them. `error.message` says why: \"This policy isn't published right now, so there is nothing to acknowledge until HR publishes it again.\" for a draft, \"This policy is no longer in effect, so there is nothing to acknowledge.\" otherwise. Do not retry; drop the item. Anyone without a row gets the `404` instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policy_hub/policies/{id}/acknowledge": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The POLICY's id (from the list, the detail or `pending[].id`).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "Acknowledge (accept) a policy",
        "description": "The caller accepts the current version of a published policy that asks\nthem for an acknowledgment. This is a signature: the request's IP\naddress, user agent and device are recorded with it (door `api`,\nsignature method `click`), the row is pinned to the policy's current\nversion, marked viewed if it was not yet, and any re-acknowledgment\nflag and deadline are cleared. No request body.\n\n**The response is the acknowledgment row, not the policy:**\n`{ \"acknowledgment\": PolicyHubAcknowledgment }` — `acknowledgment.id` is\nthe acknowledgment's own id and `acknowledgment.policy_id` the policy's.\nIt carries `acknowledged_at` and `viewed_at` (the same names\n`GET /policy_hub/policies/{id}` uses; there is no `first_viewed_at`),\nso a client can repaint the reader's \"Your Activity\" section from it\nwithout re-reading the policy. Like the policy detail it is rendered\nbare — no `unread_notification_count`, no `_meta`.\n\n**Idempotent.** A policy the caller has already accepted (and owes no\nre-acknowledgment for) answers `200` with the existing acknowledgment\nunchanged; a row HR flagged for re-acknowledgment is accepted again.\n\nChecked in this order, each refusal stopping the request: the token's\nscope, Policy Hub enabled and accessible, a View-as session\n(`view_as`), the policy exists and the caller may read it, it is\npublished (`410 policy_withdrawn` for someone it once asked), it asks\nfor an acknowledgment (`not_applicable`), then — before any gate below\n— a caller who already accepted it and is not asked again gets `200`\nwith that acknowledgment, it is for the caller (`forbidden` — asked\nonly when the caller has no acknowledgment row yet: an existing row is\nthe assignment and is always completable), required training is\ncomplete (`training_required`), a\ncomprehension check is passed (`comprehension_required` — there is no\nquiz in this API; take it on the web), and it is not a policy the\ncaller must SIGN (`esignature_required`, with `details.sign_url`).\n`GET /policy_hub/policies/{id}` names these steps in advance\n(`requires_signature`, `comprehension_required`, `required_training`).\n\n**Required Scopes:** `write:notifications` — the scope every\nacknowledgment asks (#5068), not `read:policy_hub` and not\n`write:policy_hub`. A token without it gets `403\ninsufficient_permissions` (\"Required scope: write:notifications\")\nbefore anything else is checked. The sign-in tokens hold it; a browser\nsession is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's acknowledgment of the policy, as now stored — `acknowledged: true` with `acknowledged_at` and `viewed_at` set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "acknowledgment"
                  ],
                  "properties": {
                    "acknowledgment": {
                      "$ref": "#/components/schemas/PolicyHubAcknowledgment"
                    }
                  }
                },
                "example": {
                  "acknowledgment": {
                    "id": 313372,
                    "policy_id": 60,
                    "policy_title": "Code of Conduct",
                    "acknowledged": true,
                    "acknowledged_at": "2026-10-09T09:03:11.482-04:00",
                    "viewed_at": "2026-10-09T09:01:47.106-04:00",
                    "requires_re_acknowledgment": false,
                    "signed_earlier_version": false,
                    "deadline": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:notifications\"), `feature_not_enabled` (Policy Hub is not enabled for the business), `app_forbidden` (the caller has no access to Policy Hub), `view_as` (an administrator viewing as this person — only they can acknowledge or sign a policy, signed in as themselves) or `forbidden` (\"This policy does not apply to you.\" — the caller has no acknowledgment row for it and is not in its audience).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — \"The requested resource was not found.\" for an id that does not exist in the business or a read-restricted policy the caller may not read; \"Policy not found.\" for one that is not published and never asked the caller (no acknowledgment row — otherwise `410`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`policy_withdrawn` — the policy is no longer published (a draft, archived or retired) and the caller holds an acknowledgment row on it, so it once asked them. `error.message` says why: \"This policy isn't published right now, so there is nothing to acknowledge until HR publishes it again.\" for a draft, \"This policy is no longer in effect, so there is nothing to acknowledge.\" otherwise. Do not retry; drop the item. Anyone without a row gets the `404` instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` — \"We couldn't record your acknowledgment. Please try again.\" A concurrent request created the acknowledgment row at the same moment and it could not be re-read. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Not accepted. The standard error envelope with one of these codes:\n\n* `not_applicable` — \"This policy does not require acknowledgment.\"\n* `training_required` — \"You must complete the required training\n  before acknowledging this policy: <titles>.\" When the caller\n  cannot open the Training app, the message goes on to say why and\n  who can fix it (the detail's `training_access_notice`).\n  `error.details.policy_url` is the policy page listing the courses.\n* `comprehension_required` — the policy has a comprehension check\n  the caller has not passed; complete it from the web app.\n* `esignature_required` — \"This policy asks for your signature.\n  Open it in Policy Hub to sign.\" `error.details.sign_url` is the\n  absolute URL of the signing step\n  (`/apps/policy-hub/acknowledge/{id}/sign`, which continues a\n  signing already under way); open it in a browser.\n* `acknowledgment_failed` — \"We couldn't record your\n  acknowledgment. Please try again.\"\n\nOr, when the acknowledgment row fails validation, the validation\nenvelope `{ \"errors\": [ { \"field\", \"message\", \"code\":\n\"validation_error\" } ] }` instead.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrors"
                    }
                  ]
                },
                "examples": {
                  "esignature_required": {
                    "value": {
                      "error": {
                        "code": "esignature_required",
                        "message": "This policy asks for your signature. Open it in Policy Hub to sign.",
                        "details": {
                          "sign_url": "https://acme.workforce.mangoapps.com/apps/policy-hub/acknowledge/60/sign"
                        }
                      }
                    }
                  },
                  "not_applicable": {
                    "value": {
                      "error": {
                        "code": "not_applicable",
                        "message": "This policy does not require acknowledgment."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/policy_hub/handbooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "Read one handbook",
        "description": "A published handbook aimed at the caller — exactly the handbooks\n`GET /policy_hub/handbooks` lists (for all employees, or targeted at a\ndepartment or location the caller belongs to) — with its introduction\nand its sections in order. A policy section names its policy by\n`policy_id` and carries no body: open it at\n`GET /policy_hub/policies/{policy_id}`. A policy section whose policy is\nno longer published, or that the caller may not read, is left out, so\nevery `policy_id` here opens. Policy Hub must be enabled for the\nbusiness and accessible to the caller.\n\n**Required Scopes:** `read:policy_hub` (#7183) — the scope of every\nPolicy Hub read. A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:policy_hub\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The handbook, under a `handbook` key and nothing else (`render_single`: no `unread_notification_count`, no `_meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "handbook"
                  ],
                  "properties": {
                    "handbook": {
                      "$ref": "#/components/schemas/PolicyHubHandbookDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:policy_hub\" — the token lacks the scope), `feature_not_enabled` (Policy Hub is not enabled for the business) or `app_forbidden` (the caller has no access to Policy Hub).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — \"Handbook not found.\" The same answer for an id that does not exist, a handbook that is not published, and one not aimed at the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policy_hub/acknowledgments": {
      "get": {
        "tags": [
          "Policy Hub"
        ],
        "summary": "Your acknowledgments (pending and history)",
        "description": "The caller's compliance inbox, as two independently paged collections\nin one response:\n\n* `pending` — the published policies still asking the caller for an\n  acknowledgment: never accepted, or flagged by HR for\n  re-acknowledgment, and only while the policy's CURRENT audience\n  includes the caller. Each entry is the policy summary (its `id` is the\n  POLICY id — acknowledge it at\n  `POST /policy_hub/policies/{id}/acknowledge`) plus the deadline state\n  of the acknowledgment that makes it pending. Most recently changed\n  policy first (policy `updated_at` descending, then policy `id`\n  descending). Paged by `pending_page`; `pending_meta` describes it.\n* `history` — the caller's acknowledgments (`acknowledged: true`),\n  most recently accepted first (`acknowledged_at` descending, then\n  `id` descending). It includes a row HR has since flagged; read\n  `requires_re_acknowledgment` (asked again — the policy also appears\n  in `pending`) and `signed_earlier_version` (flagged, but no longer\n  asked). Its policy may since have been unpublished, in which case\n  `GET /policy_hub/policies/{policy_id}` answers 404. Paged by `page`;\n  `meta`, the root `total_count` and the `X-Total-Count` /\n  `X-Total-Pages` / `X-Current-Page` / `X-Per-Page` headers describe it.\n\n* `campaign_packages` — HR's signature campaigns waiting for the\n  caller: a campaign asks for policies the caller may hold no\n  acknowledgment for, so they are not in `pending`. Each is signed in\n  the browser at its `sign_path`. Most recent first, at most `per_page`.\n\nOne `per_page` sizes both pages. Policy Hub must be enabled for the\nbusiness and accessible to the caller.\n\n**Required Scopes:** `read:policy_hub` (#7183) — the scope of every\nPolicy Hub read. A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:policy_hub\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page of `history`.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "pending_page",
            "in": "query",
            "required": false,
            "description": "Page of `pending` — its own cursor, independent of `page`.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page of BOTH `pending` and `history`. Values above 100 are clamped to 100; an empty, zero, negative or non-numeric value uses the default.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of each collection",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pending",
                    "pending_meta",
                    "history",
                    "meta",
                    "total_count"
                  ],
                  "properties": {
                    "campaign_packages": {
                      "type": "array",
                      "description": "Signature campaigns waiting for the caller (not in `pending`).",
                      "items": {
                        "type": "object",
                        "required": [
                          "package_id",
                          "campaign",
                          "sign_path"
                        ],
                        "properties": {
                          "package_id": {
                            "type": "integer"
                          },
                          "campaign": {
                            "type": "string",
                            "description": "The campaign's name."
                          },
                          "sign_path": {
                            "type": "string",
                            "description": "Where the caller signs it, in the browser."
                          }
                        }
                      }
                    },
                    "pending": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PolicyHubPendingAcknowledgment"
                      }
                    },
                    "pending_meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      ],
                      "description": "Pagination of `pending` (cursor `pending_page`)."
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PolicyHubAcknowledgment"
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      ],
                      "description": "Pagination of `history` (cursor `page`)."
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "The caller's accepted acknowledgments across all pages (`meta.total_count`)."
                    },
                    "unread_notification_count": {
                      "$ref": "#/components/schemas/UnreadNotificationCount"
                    },
                    "_meta": {
                      "$ref": "#/components/schemas/PlatformHttpMeta"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:policy_hub\" — the token lacks the scope), `feature_not_enabled` (Policy Hub is not enabled for the business) or `app_forbidden` (the caller has no access to Policy Hub).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/dashboard": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "SOP Hub home for the caller",
        "description": "Counts, the caller's top five pending completions (soonest deadline\nfirst), five most recent completions, up to five manuals they may open,\nand what they may do in SOP Hub. SOP Hub must be enabled for the\nbusiness and open to the caller (403 `app_not_enabled` /\n`app_not_accessible`).\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The dashboard.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "admin": {
                      "type": "object",
                      "nullable": true,
                      "description": "The author's panel — the web dashboard's own numbers, over the caller's reach. Present only for a SOP Hub author (HR, an admin, a SOP Hub app admin, or one limited to places and departments); null for everyone else.",
                      "properties": {
                        "library": {
                          "$ref": "#/components/schemas/SopHubStatusCounts"
                        },
                        "manuals": {
                          "type": "object",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "published": {
                              "type": "integer"
                            }
                          }
                        },
                        "completions": {
                          "type": "object",
                          "properties": {
                            "completed": {
                              "type": "integer",
                              "description": "The Completions card's number: signed off where sign-off is required, by current members, on published SOPs."
                            },
                            "pending": {
                              "type": "integer",
                              "description": "Owed by current members on published SOPs that ask for completion."
                            },
                            "awaiting_sign_off": {
                              "type": "integer",
                              "description": "The Awaiting Verification list's count (`GET /sop_hub/completions?status=pending_verification`)."
                            }
                          }
                        },
                        "recent_sops": {
                          "type": "array",
                          "description": "The five most recently changed SOPs, any status.",
                          "items": {
                            "$ref": "#/components/schemas/SopHubLibraryRow"
                          }
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "properties": {
                        "pending": {
                          "type": "integer"
                        },
                        "overdue": {
                          "type": "integer"
                        },
                        "completed": {
                          "type": "integer",
                          "description": "Signed off where sign-off is required."
                        },
                        "published_sops": {
                          "type": "integer"
                        },
                        "manuals": {
                          "type": "integer",
                          "description": "The published manuals meant for the caller (an author's own drafts are counted under `admin.manuals`)."
                        }
                      }
                    },
                    "my_pending": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubCompletion"
                      }
                    },
                    "recently_completed": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubCompletion"
                      }
                    },
                    "manuals": {
                      "type": "array",
                      "description": "The Procedure Manuals cards, up to five by title: the published manuals meant for the caller and, for a SOP Hub author, also the manuals they manage in every status (`status` labels a draft or an archived one; `GET /sop_hub/manuals/{id}` opens it for them). `sop_count` counts every SOP section of a manual the caller manages, and the sections they may open of any other.",
                      "items": {
                        "$ref": "#/components/schemas/SopHubManual"
                      }
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "can_view_reports": {
                          "type": "boolean"
                        },
                        "can_manage_library": {
                          "type": "boolean"
                        },
                        "can_verify": {
                          "type": "boolean"
                        },
                        "can_remind": {
                          "type": "boolean",
                          "description": "An author",
                          "with completion reminders switched on.": null
                        },
                        "can_manage_settings": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or invalid token."
          },
          "403": {
            "description": "Missing scope",
            "or SOP Hub is off or closed to the caller.": null
          }
        }
      }
    },
    "/sop_hub/completions/my_pending": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "My pending completions",
        "description": "What the caller still owes, newest first — the list desktop My SOPs and\nthe phone page show.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of completions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubCompletion"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/completions/my_history": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "My completion history",
        "description": "Every completion the caller has submitted, newest first. Keeps a record\nwhose SOP was since archived or retired (`sop.status` says so) and one\nre-opened for re-completion (`status` says so).\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of completions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubCompletion"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/sops/{id}": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Read a SOP",
        "description": "A published SOP the caller may read, in the requested language (`lang`,\nelse their own, else English), with the caller's own completion. A SOP\noutside their audience opens read-only (`capabilities.reference_only`);\nan optional SOP (no completion required) in their audience can be worked\nthrough but is never owed.\nA SOP that is not published (a draft, or withdrawn) answers 422 `sop_not_published`; one they may not read\nanswers 404.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The SOP.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SopHubSopSummary"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "language": {
                              "type": "string"
                            },
                            "available_languages": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "code": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              }
                            },
                            "steps": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "position": {
                                    "type": "integer"
                                  },
                                  "title": {
                                    "type": "string"
                                  },
                                  "content": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "estimated_minutes": {
                                    "type": "integer",
                                    "nullable": true
                                  },
                                  "verification_required": {
                                    "type": "boolean"
                                  },
                                  "media_url": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "safety": {
                                    "$ref": "#/components/schemas/SopHubStepSafety"
                                  },
                                  "completed": {
                                    "type": "boolean"
                                  },
                                  "changed_since_my_last_completion": {
                                    "type": "boolean",
                                    "description": "The step was added or changed since the version the caller completed (the web's \"Changed since your last completion\" badge)."
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    },
                    "my_completion": {
                      "$ref": "#/components/schemas/SopHubCompletion"
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "can_complete": {
                          "type": "boolean"
                        },
                        "reference_only": {
                          "type": "boolean"
                        }
                      }
                    },
                    "required_training": {
                      "type": "array",
                      "description": "Required courses the caller has not finished.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          },
                          "met": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or not one the caller may read.": null
          },
          "422": {
            "description": "`sop_not_published` — the SOP is not published: still a draft (\"isn't published right now\") or withdrawn (\"no longer in use\")."
          }
        }
      }
    },
    "/sop_hub/sops/{id}/start": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Start a SOP",
        "description": "Creates the caller's completion (or returns the one they have).\n`completion_method` is set by the server. Refusals: 403\n`not_in_audience`, 403 `attestation_not_delegable` (View-as or an\nimpersonation token), 422 `training_prerequisite_required`.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's completion.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completion": {
                      "$ref": "#/components/schemas/SopHubCompletion"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Refused",
            "see description.": null
          },
          "422": {
            "description": "Refused",
            "see description.": null
          }
        }
      }
    },
    "/sop_hub/sops/{id}/steps/{step_position}/complete": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Complete a step",
        "description": "Records step `step_position` (0-based). Idempotent: completing the same\nposition again replaces its record. Starts the completion first when the\nSOP applies to the caller. Refusals as for start, plus 422 `invalid_step`\nand 422 `notes_too_long`.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          },
          {
            "name": "step_position",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated completion. A SOP with no read-and-understood acknowledgement is submitted by its last step: `submitted` is true and `message` says so. A step on work already on the record answers `already_recorded: true` with its `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completion": {
                      "$ref": "#/components/schemas/SopHubCompletion"
                    },
                    "submitted": {
                      "type": "boolean"
                    },
                    "already_recorded": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/sops/{id}/complete": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Submit a SOP",
        "description": "Submits the caller's completion once every step is recorded. A SOP that\nasks for an acknowledgement needs `acknowledged: true` (a JSON boolean, or\nthe string \"true\" in a form body). Refusals (422):\n`not_started`, `steps_incomplete`, `acknowledgement_required`,\n`completion_failed`, plus those of start. A SOP that needs sign-off\nanswers `status: pending_verification`. A repeat of a submit that already\ncounted (a second tap, a replay, one waiting for its sign-off) is a 200\nwith `already_recorded: true` and the web's sentence in `message`.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "acknowledged": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The submitted completion.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completion": {
                      "$ref": "#/components/schemas/SopHubCompletion"
                    },
                    "already_recorded": {
                      "type": "boolean",
                      "description": "True when the work was already on the record; nothing new was written."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/manuals": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Manuals I may open",
        "description": "Published procedure manuals aimed at the caller, newest change first. A\nSOP Hub author gets, by title, those and also the manuals they manage in\nevery status (the web's manuals list) — `status` labels a draft or an\narchived one, `GET /sop_hub/manuals/{id}` opens it for them, and\n`sop_count` counts every SOP section of a manual they manage (the\nsections they may open of any other).\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of manuals.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manuals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubManual"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/manuals/{id}": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Read a manual",
        "description": "A published manual meant for the caller, with its sections in order; a\nsection whose SOP the caller cannot open is left out. Someone who manages\nthe manual (a SOP Hub author, inside a scoped admin's reach) opens it in\nANY status — a draft or an archived one — with every section, as the web\nmanual page shows it to them, and gets its `audience_targets` (`can_manage` true).\n\nThe text under a SOP section's card on the web and the phone is the\nprocedure's own description: `sop.description`.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The manual.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manual": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SopHubManual"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "created_by": {
                              "$ref": "#/components/schemas/SopHubPerson"
                            },
                            "footer": {
                              "type": "string",
                              "nullable": true,
                              "description": "The manual's footer text."
                            },
                            "created_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "The Overview's Created."
                            },
                            "updated_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "The Overview's Last Updated."
                            },
                            "is_public": {
                              "type": "boolean",
                              "description": "The header's Public label: the public link opens — the manual is published, shared, has a link, and the business allows public SOP and manual URLs."
                            },
                            "can_manage": {
                              "type": "boolean",
                              "description": "The caller manages this manual (any status opens; every section is listed)."
                            },
                            "audience_targets": {
                              "type": "object",
                              "nullable": true,
                              "description": "Who the manual is for — only when `can_manage`. Everyone, or the departments, locations and roles picked (a reader must match every axis picked); none picked and not everyone means nobody.",
                              "properties": {
                                "all_employees": {
                                  "type": "boolean"
                                },
                                "departments": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                },
                                "locations": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                },
                                "roles": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "integer"
                                      },
                                      "name": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "sections": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "position": {
                                    "type": "integer"
                                  },
                                  "section_type": {
                                    "type": "string",
                                    "description": "Open set; today sop, custom, chapter_header, divider."
                                  },
                                  "title": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "content": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "show_in_toc": {
                                    "type": "boolean"
                                  },
                                  "sop": {
                                    "description": "On a `sop` section — the procedure, with its steps for an expanded card; open it with `GET /sop_hub/sops/{id}`.",
                                    "allOf": [
                                      {
                                        "$ref": "#/components/schemas/SopHubSopSummary"
                                      },
                                      {
                                        "type": "object",
                                        "properties": {
                                          "duration_minutes": {
                                            "type": "integer",
                                            "description": "The SOP's duration",
                                            "or the sum of its steps' estimates.": null
                                          },
                                          "steps": {
                                            "type": "array",
                                            "items": {
                                              "allOf": [
                                                {
                                                  "$ref": "#/components/schemas/SopHubStep"
                                                },
                                                {
                                                  "type": "object",
                                                  "properties": {
                                                    "position": {
                                                      "type": "integer"
                                                    }
                                                  }
                                                }
                                              ]
                                            }
                                          }
                                        }
                                      }
                                    ]
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or not one the caller may open.": null
          }
        }
      }
    },
    "/sop_hub/manuals/{id}/pdf": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Download a manual as a PDF",
        "description": "The manual as the web's Download PDF prints it: the same manuals as\n`GET /sop_hub/manuals/{id}`, in its document theme. A published manual\nprints the procedures a reader may open, for everyone; only someone who\nmanages an unpublished manual gets every section, each procedure readers\nwould not get stamped with its status.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The PDF, sent as an attachment.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or not one the caller may open.": null
          }
        }
      }
    },
    "/sop_hub/browse": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Browse SOPs",
        "description": "Every published SOP the caller may read, most recently changed first —\nthe web's Browse SOPs, open to every member. `assigned_to_me` says the\ncaller owes it (it opens for completion with `GET /sop_hub/sops/{id}`).\nAn optional SOP in the caller's audience is never `assigned_to_me` but can\nstill be worked through; a SOP outside their audience opens read-only. `filters.sop_types` are the type pills,\neach counted with the search applied.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": null,
            "tags).": null
          },
          {
            "name": "sop_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "One type; an unknown type is ignored."
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of SOPs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sops": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/SopHubLibraryRow"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "assigned_to_me": {
                                "type": "boolean"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "filters": {
                      "type": "object",
                      "properties": {
                        "sop_types": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SopHubFilterOption"
                          }
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/sops": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "The SOP library",
        "description": "Every SOP the caller authors, most recently changed first, with each\none's assigned and completed counts (people in its current audience).\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "draft, published, archived, retired (open set)."
          },
          {
            "name": "sop_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search text."
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of SOPs, the status chips' counts and the filter sheet's options.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "counts": {
                      "$ref": "#/components/schemas/SopHubStatusCounts"
                    },
                    "filters": {
                      "type": "object",
                      "description": "The filter sheet's Type and Category options, in the order the web lists them, each counted with the caller's other filters applied (its own left out) — the web list's facet rule.",
                      "properties": {
                        "sop_types": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SopHubFilterOption"
                          }
                        },
                        "categories": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SopHubFilterOption"
                          }
                        }
                      }
                    },
                    "sops": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/SopHubLibraryRow"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "completion_counts": {
                                "type": "object",
                                "properties": {
                                  "assigned": {
                                    "type": "integer"
                                  },
                                  "completed": {
                                    "type": "integer",
                                    "description": "Signed off where sign-off is required."
                                  }
                                }
                              },
                              "can_remind": {
                                "type": "boolean"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an author",
            "or a missing scope.": null
          }
        }
      }
    },
    "/sop_hub/sops/{id}/admin": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "A SOP as its author sees it",
        "description": "One SOP in any status — the web author page: its steps (a draft's or an\narchived SOP's too), the In Short summary, the Completion Stats card, the\nDetails card, who owns it, its QR code and its five latest versions.\n`stats` counts the completions of people in the SOP's current audience;\ncompleted + awaiting_sign_off + pending = total.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          }
        ],
        "responses": {
          "200": {
            "description": "The SOP.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SopHubLibraryRow"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "created_at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "created_by": {
                              "$ref": "#/components/schemas/SopHubPerson"
                            },
                            "requires_completion": {
                              "type": "boolean"
                            },
                            "duration_minutes": {
                              "type": "integer",
                              "nullable": true,
                              "description": "The SOP's duration, or the sum of its steps' estimates; null when neither is set."
                            },
                            "verification": {
                              "type": "object",
                              "properties": {
                                "required": {
                                  "type": "boolean",
                                  "description": "A finished completion waits for sign-off."
                                },
                                "whole_sop": {
                                  "type": "boolean",
                                  "description": "The SOP itself asks for sign-off."
                                },
                                "steps": {
                                  "type": "array",
                                  "items": {
                                    "type": "integer"
                                  },
                                  "description": "Step numbers that ask for sign-off: each is a step's `position` + 1."
                                }
                              }
                            },
                            "audience": {
                              "type": "string",
                              "description": "Who it is for, in the web's words and the caller's language (\"Everyone\" when it applies to all employees)."
                            },
                            "ai_summary": {
                              "type": "string",
                              "nullable": true,
                              "description": "Machine-written summary of the steps; the steps are the procedure. Null when none was written."
                            },
                            "owner": {
                              "type": "object",
                              "properties": {
                                "person": {
                                  "$ref": "#/components/schemas/SopHubPerson"
                                },
                                "group": {
                                  "type": "object",
                                  "nullable": true,
                                  "properties": {
                                    "id": {
                                      "type": "integer"
                                    },
                                    "name": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "qr_code": {
                              "type": "string",
                              "nullable": true,
                              "description": "Null while the tenant's QR code access is off."
                            },
                            "steps": {
                              "type": "array",
                              "items": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/SopHubStep"
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "position": {
                                        "type": "integer"
                                      }
                                    }
                                  }
                                ]
                              }
                            }
                          }
                        }
                      ]
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "completed": {
                          "type": "integer",
                          "description": "Signed off where sign-off is required."
                        },
                        "awaiting_sign_off": {
                          "type": "integer"
                        },
                        "pending": {
                          "type": "integer",
                          "description": "Not finished",
                          "or owed again.": null
                        },
                        "rate": {
                          "type": "number",
                          "description": "Completed as a percent of total, one decimal; 0 when total is 0."
                        }
                      }
                    },
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubVersion"
                      }
                    },
                    "total_versions": {
                      "type": "integer",
                      "description": "All versions; read them with GET /sop_hub/sops/{id}/versions."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an author",
            "or a missing scope.": null
          },
          "404": {
            "description": "Not found",
            "or outside the caller's reach.": null
          }
        }
      }
    },
    "/sop_hub/sops/{id}/versions": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "A SOP's version history",
        "description": "Newest version first.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "The versions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop": {
                      "$ref": "#/components/schemas/SopHubSopSummary"
                    },
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubVersion"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or outside the caller's reach.": null
          }
        }
      }
    },
    "/sop_hub/sops/{id}/versions/compare": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Compare two versions",
        "description": "The server's diff of two versions: added, removed and modified steps\n(with each changed field), and the title and description when they differ.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Version number."
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Version number."
          }
        ],
        "responses": {
          "200": {
            "description": "The comparison.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop": {
                      "$ref": "#/components/schemas/SopHubSopSummary"
                    },
                    "from": {
                      "$ref": "#/components/schemas/SopHubVersion"
                    },
                    "to": {
                      "$ref": "#/components/schemas/SopHubVersion"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total_changes": {
                          "type": "integer"
                        },
                        "added_count": {
                          "type": "integer"
                        },
                        "removed_count": {
                          "type": "integer"
                        },
                        "modified_count": {
                          "type": "integer"
                        },
                        "unchanged_count": {
                          "type": "integer"
                        },
                        "step_count_change": {
                          "type": "integer"
                        },
                        "change_percentage": {
                          "type": "number"
                        }
                      }
                    },
                    "steps": {
                      "type": "object",
                      "properties": {
                        "added": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer"
                              },
                              "step": {
                                "$ref": "#/components/schemas/SopHubStep"
                              }
                            }
                          }
                        },
                        "removed": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer"
                              },
                              "step": {
                                "$ref": "#/components/schemas/SopHubStep"
                              }
                            }
                          }
                        },
                        "modified": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "index": {
                                "type": "integer"
                              },
                              "before": {
                                "$ref": "#/components/schemas/SopHubStep"
                              },
                              "after": {
                                "$ref": "#/components/schemas/SopHubStep"
                              },
                              "changes": {
                                "type": "array",
                                "description": "Each changed field, its label and its old and new value as the step reads them (a hazard as \"Danger: Stored energy\", a flag as Yes or No), never stored JSON.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "field": {
                                      "type": "string"
                                    },
                                    "label": {
                                      "type": "string",
                                      "description": "What the field reads as, e.g. PPE for required_ppe."
                                    },
                                    "old": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "new": {
                                      "type": "string",
                                      "nullable": true
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "text_changes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "field": {
                            "type": "string"
                          },
                          "before": {
                            "type": "string",
                            "nullable": true
                          },
                          "after": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The SOP or a version is not found (`version_not_found`)."
          },
          "422": {
            "description": "Two versions are needed (`versions_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/completions": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Completions to review",
        "description": "Completions across the caller's SOPs, newest completion first on every\n`status` (a sign-off request by when it was submitted), rows never\nfinished after them — as the web and /m/ lists order it. `status` filters to\none of the web list's pills: `completed` (signed off where required),\n`pending_verification` (awaiting sign-off) or `not_completed` (not\nfinished, or owed again) — together they partition the list.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "sop_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A person's name (first, last, preferred or full), as on the web list. Never matched against email."
          },
          {
            "name": "completed_from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Completed on or after this day (YYYY-MM-DD); an unreadable date is ignored."
          },
          {
            "name": "completed_to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Completed on or before this day (YYYY-MM-DD)."
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of completions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopHubReviewCompletion"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "The status chips, counted before `status` narrows the list (`sop_id` applies). total = completed + pending_verification + not_completed.",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "description": "All completions (the All chip)."
                        },
                        "completed": {
                          "type": "integer"
                        },
                        "pending_verification": {
                          "type": "integer"
                        },
                        "not_completed": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`sop_id` is not a SOP in the caller's reach."
          }
        }
      }
    },
    "/sop_hub/completions/export": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Export completions as CSV",
        "description": "The completions list as a CSV file, unpaginated — the web list's Export:\nthe same filters as `GET /sop_hub/completions` (`sop_id`, `status`, `q`,\n`completed_from`, `completed_to`), one row per completion, with the\ncolumns Employee, Email, SOP, Status, Steps done, Completed at, Version\ncompleted, Signed off by, Signed off at, Read and understood at, Recorded\nthrough, Notes, Step notes and Sign-off note. The Email column is filled\nonly where the People directory shows the caller email.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "sop_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "completed_from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "completed_to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The CSV, sent as an attachment (sop-completions[-<sop>]-<date>.csv).",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "Not an author",
            "or a missing scope.": null
          },
          "404": {
            "description": "`sop_id` is not a SOP in the caller's reach."
          }
        }
      }
    },
    "/sop_hub/completions/{id}/verify": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Sign a completion off",
        "description": "Records the caller's sign-off, tells the worker (when the tenant keeps\n\"Confirm Sign-Off Back to the Employee\" on) and writes the activity-log\nrow — the web's own Verify. Refused with `cannot_verify`;\n`error.details.reason` says why (open set: own_completion,\nre_completion_required, already_verified, procedure_retired,\nsign_off_not_required, cannot_verify). 403 for your own completion or one\nowed again, 422 for the other states.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed off.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completion": {
                      "$ref": "#/components/schemas/SopHubReviewCompletion"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an author, or a sign-off the caller may not give.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or outside the caller's reach.": null
          },
          "422": {
            "description": "The completion is in a state that cannot be signed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/completions/{id}/send_back": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Send a completion back to be redone",
        "description": "Re-opens a completion awaiting sign-off and tells the worker the reason\n(required, at most 1,000 characters: `send_back_reason_required`,\n`send_back_reason_too_long`). Other refusals are `cannot_send_back` with\n`error.details.reason` (open set).\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent back.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "completion": {
                      "$ref": "#/components/schemas/SopHubReviewCompletion"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an author",
            "or the caller's own completion.": null
          },
          "404": {
            "description": "Not found",
            "or outside the caller's reach.": null
          },
          "422": {
            "description": "No reason, or the completion cannot be sent back.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/reports/compliance": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Compliance report",
        "description": "One row per published SOP that asks for completion, by title: its\naudience, signed-off, awaiting sign-off and pending counts, the rate\n(null when nobody is in the audience) and how many reminders a Remind\nwould send now. `reminders` states the tenant's reminder rule.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "sop": {
                            "$ref": "#/components/schemas/SopHubSopSummary"
                          },
                          "target_users": {
                            "type": "integer"
                          },
                          "completed": {
                            "type": "integer"
                          },
                          "awaiting_sign_off": {
                            "type": "integer"
                          },
                          "pending": {
                            "type": "integer"
                          },
                          "completion_rate": {
                            "type": "number",
                            "nullable": true
                          },
                          "remindable": {
                            "type": "integer"
                          },
                          "audience_warning": {
                            "type": "string",
                            "nullable": true,
                            "description": "Why the audience shrank — some or all of its groups were deleted — in the caller's language; null when none was."
                          },
                          "audience_groups": {
                            "type": "object",
                            "nullable": true,
                            "description": "The numbers behind audience_warning, for a client that words it itself; null when no group is gone.",
                            "properties": {
                              "missing": {
                                "type": "integer",
                                "description": "Groups in the audience that were deleted."
                              },
                              "total": {
                                "type": "integer",
                                "description": "Groups the audience names."
                              }
                            }
                          },
                          "last_reminded_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "The latest reminder sent on this SOP's current completions (a re-opened completion starts its reminders afresh)."
                          },
                          "reminded_today": {
                            "type": "boolean",
                            "description": "last_reminded_at falls on today in the business's time zone (the \"Reminded today\" pill)."
                          },
                          "can_remind": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "reminders": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "disabled_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why reminders are off, in the caller's language and in words they can act on (turn them on in Settings, or ask an HR admin or administrator); null while they are on."
                        },
                        "none_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why Send All Reminders has nobody to send to while reminders are on (nobody who can be reminded owes a SOP, or everyone who does was reminded in the last frequency_days days or is at max_reminders), in the caller's language — the line the web draws under its disabled button. Null when reminders are off or someone can be reminded."
                        },
                        "frequency_days": {
                          "type": "integer"
                        },
                        "max_reminders": {
                          "type": "integer"
                        },
                        "remindable_total": {
                          "type": "integer"
                        },
                        "can_remind_all": {
                          "type": "boolean"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/sops/{id}/remind": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Remind everyone who owes a SOP",
        "description": "Queues reminders for the SOP's pending completions, held to the tenant's\nreminder frequency and the reminder limit. Refused with 422\n`reminders_disabled` while reminders are switched off.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SopId"
          }
        ],
        "responses": {
          "200": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubReminderDispatch"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "or outside the caller's reach.": null
          },
          "422": {
            "description": "Reminders are switched off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/reports/remind_all": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Remind every pending completion",
        "description": "Queues reminders for every pending completion the caller reviews (a\nscoped admin's: those in their reach), under the same throttle.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubReminderDispatch"
                }
              }
            }
          },
          "422": {
            "description": "Reminders are switched off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/activity_logs": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "SOP Hub activity log",
        "description": "The audit trail, newest first. `event_type` is an open set of strings;\neach row's `label` and the `event_types` list (the filter's options) are\nthe web Activity Log's words.\n\nAuthors only (403 `sop_hub_author_required`); a scoped admin sees the SOPs in their reach.\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "event_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sop_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of log rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "activity_logs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "event_type": {
                            "type": "string",
                            "description": "Open set."
                          },
                          "label": {
                            "type": "string",
                            "description": "The event's name, in the caller's language."
                          },
                          "description": {
                            "type": "string"
                          },
                          "sop": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "title": {
                                "type": "string"
                              }
                            }
                          },
                          "by": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "event_types": {
                      "type": "array",
                      "description": "Every event type the filter offers, with its label.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/SopHubPageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sop_hub/qr/{code}": {
      "get": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Resolve a scanned SOP label",
        "description": "The published SOP a label's code names, if the caller may read it, and\nwhether they may start it (`reference_only` when it is not assigned to\nthem). 403 `qr_access_disabled` while the tenant has QR access off; 422\n`sop_not_published` for a procedure that is not published (a draft, or withdrawn — the message says which).\n\n**Required Scopes:** `read:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The SOP.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop": {
                      "$ref": "#/components/schemas/SopHubSopSummary"
                    },
                    "my_completion": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/SopHubCompletion"
                        }
                      ],
                      "nullable": true
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "can_complete": {
                          "type": "boolean"
                        },
                        "reference_only": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No SOP the caller may read has this code."
          }
        }
      }
    },
    "/sop_hub/qr/{code}/start": {
      "post": {
        "tags": [
          "SOP Hub"
        ],
        "summary": "Start a SOP from its label",
        "description": "Creates or returns the caller's completion, recorded as a QR scan\n(`completion_method` is set by the server). Then work it through\n`/sop_hub/sops/{id}/steps/{step_position}/complete` and\n`/sop_hub/sops/{id}/complete`, which keep that method.\n\n**Required Scopes:** `write:sop_hub`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The completion.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sop_id": {
                      "type": "integer"
                    },
                    "completion": {
                      "$ref": "#/components/schemas/SopHubCompletion"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not in the SOP's audience (`not_in_audience`)",
            "or QR access is off.": null
          },
          "422": {
            "description": "Required training first (`training_prerequisite_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SopHubError"
                }
              }
            }
          }
        }
      }
    },
    "/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, 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`. A disabled library the caller is in the audience of but cannot manage answers the same status with the code `library_disabled` and the message \"\\\"<library name>\\\" has been disabled and isn't available right now.\" — that caller already knows it exists."
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04): requires the `destructive:libraries`\nscope.\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. A disabled library the caller is in the audience of but cannot manage answers `library_disabled` (\"\\\"<library name>\\\" has been disabled and isn't available right now.\")."
          },
          "422": {
            "description": "The delete was refused by the record itself (`delete_failed`). The message carries the model's own reason."
          }
        }
      }
    },
    "/libraries/{library_id}/items/{id}/captions": {
      "parameters": [
        {
          "name": "library_id",
          "in": "path",
          "required": true,
          "description": "The library the item belongs to. The item is looked up THROUGH it.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The library ITEM id (a video).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Libraries"
        ],
        "summary": "Get a video item's caption track",
        "description": "The WebVTT caption track of one VIDEO item, for the native player — the\nbearer-token twin of the web player's `<track>` route\n(`Apps::Libraries::CaptionsController#show`), which answers only a\nbrowser session. Every item card carries this URL as `captions_url`\nwhen its `captions` marker is `available`.\n\n**Authorization — the web player's, unchanged.** Anyone who can view\nthe library (and, when it is disabled, only those who manage it). Needs\nthe `read:libraries` scope. This is a READ on the same floor as\n`GET /libraries/{id}`, not one of the management writes above.\n",
        "operationId": "getLibraryItemCaptions",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caption track.",
            "content": {
              "text/vtt": {
                "schema": {
                  "type": "string",
                  "example": "WEBVTT\n\n00:00:00.000 --> 00:00:03.000\nWelcome to the brand guidelines."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API token (`authentication_required` / `invalid_token`)."
          },
          "403": {
            "description": "Missing the `read:libraries` scope (`insufficient_permissions`), or the Libraries app is not accessible to the caller (`access_denied`)."
          },
          "404": {
            "description": "No such library you may view (`not_found`), no such item in that library (`not_found`), the item is not a video or has no caption track (`not_found` — \"Library item <id> has no captions\"), or a disabled library you cannot manage (`library_disabled`)."
          }
        }
      }
    },
    "/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 management of\" (ownership_changed), or \"updated\" for anything else.",
                                "example": "published"
                              },
                              "transferred_to": {
                                "type": "string",
                                "nullable": true,
                                "description": "The new manager's name — present only for a management 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`), or the token lacks `read:wikis` (error code `insufficient_permissions`)."
          }
        }
      }
    },
    "/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`, `mentions` (pages where\n  someone @mentioned the caller that they have not opened since — the web\n  and phone Mentions filter), 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 | mentions | stale | archived. `mentions` lists the pages where someone @mentioned the caller that they have not opened since. `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",
                "mentions",
                "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/mentions/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."
                        },
                        "mentions": {
                          "type": "integer",
                          "example": 1,
                          "description": "Every persona. Pages where someone @mentioned the caller that they have not opened since."
                        },
                        "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`), or the token lacks `read:wikis` (error code `insufficient_permissions`)."
          }
        }
      }
    },
    "/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`), or the token lacks `read:wikis` (error code `insufficient_permissions`)."
          }
        }
      }
    },
    "/wikis/trash": {
      "get": {
        "tags": [
          "Wikis"
        ],
        "summary": "Trash — the caller's deleted wikis",
        "description": "The soft-deleted wikis this caller may see, most recently deleted first —\nthe native mirror of the web Trash screen (`Apps::WikisController#trash`).\n`DELETE /wikis/{id}` is a soft delete, and this endpoint plus\n`POST /wikis/{id}/restore` are how a deleted page comes back.\n\nA wiki is listed here as its own row even when it was trashed as part of a\nparent's cascade, exactly as the web Trash table lists it — so a sub-wiki\ncan be restored on its own (see the restore endpoint's\n`parent_still_trashed`).\n\n* **Authorization — who sees which rows**, mirroring the web screen's\n  three arms:\n  * a business **admin (or above)** sees every trashed wiki in the\n    business;\n  * a **delegated Wikis app admin** (Admin › Apps › App Admins) sees\n    published trashed wikis plus their own, intersected with the ordinary\n    read-visibility rule — that grant carries no read access to\n    group-restricted pages, and an unnarrowed list would disclose another\n    member's deleted draft title and author;\n  * **everyone else** sees only the wikis **they created**. This is the\n    same `created_by_id` anchor `DELETE /wikis/{id}` authorizes on, so it\n    returns strictly less than the caller was already permitted to delete\n    — a member who deletes a page tree they own can find and recover it\n    without an admin.\n* There is no \"empty\" error: a caller with nothing in the Trash gets `200`\n  with `wikis: []`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "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": "Trash retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wikis",
                    "meta"
                  ],
                  "properties": {
                    "wikis": {
                      "type": "array",
                      "description": "Trashed wikis, most recently deleted first (`id` descending breaks the tie between pages trashed together by one cascade, so paging over them is stable).",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/WikiCard"
                          },
                          {
                            "type": "object",
                            "required": [
                              "status",
                              "discarded_at",
                              "creator"
                            ],
                            "properties": {
                              "status": {
                                "type": "string",
                                "description": "The status the wiki had when it was deleted.",
                                "enum": [
                                  "draft",
                                  "published",
                                  "archived"
                                ],
                                "example": "published"
                              },
                              "discarded_at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "When the wiki was moved to Trash (ISO-8601).",
                                "example": "2026-09-05T11:42:07Z"
                              },
                              "deleted_by": {
                                "type": "string",
                                "nullable": true,
                                "description": "Who moved it to Trash (the web Trash table's \"Deleted by\"): the deleter's name, `with “<parent title>”` for a sub-wiki that went with its parent on this page of results, or null when it cannot be told.",
                                "example": "Dana Lee"
                              },
                              "creator": {
                                "type": "object",
                                "description": "The person who manages the wiki (the web Trash table's \"Managed by\").",
                                "required": [
                                  "id",
                                  "name"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "integer",
                                    "nullable": true,
                                    "example": 1
                                  },
                                  "name": {
                                    "type": "string",
                                    "example": "Dana Lee"
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "total_count": {
                          "type": "integer",
                          "description": "Trashed wikis visible to this caller.",
                          "example": 5
                        },
                        "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 — not enabled for the business, or the caller is outside the app's visibility audience (error code `access_denied`), or the token lacks `read:wikis` (error code `insufficient_permissions`)."
          }
        }
      }
    },
    "/wikis/{id}/restore": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The trashed wiki's id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Wikis"
        ],
        "summary": "Restore a wiki from Trash",
        "description": "Recovers a soft-deleted wiki — the inverse of `DELETE /wikis/{id}` and the\nnative mirror of the web \"Restore\" button\n(`Apps::Wikis::PagesController#restore` → `Wiki#undiscard!`).\n\n**The restore is recursive**: it also brings back every sub-wiki that the\ndelete cascaded, and `sub_wikis_restored` counts exactly those. A sub-wiki\nthat was deleted **separately, earlier** keeps its own deletion timestamp\nand deliberately stays in the Trash — it is not counted and does not come\nback, so restoring a parent never resurrects a page the user deleted on\npurpose.\n\nDistinct from `DELETE /wikis/{id}/archive`: archiving is a **status**, and\nan archived wiki was never deleted. This endpoint is only for wikis in the\nTrash.\n\n* **Authorization** — a **Wikis app admin** (a business admin-or-above, or\n  a delegated app admin) may restore any trashed wiki; **everyone else may\n  restore only the wikis they created**, which is the same `created_by_id`\n  anchor `DELETE /wikis/{id}` authorizes on. Deliberately not widened to\n  \"anyone who could read the page\": a member who could merely view a\n  published wiki never had the right to delete it, so they have nothing to\n  undo. A wiki outside the caller's restore rights returns `404`, never\n  revealing that it exists.\n* `parent_still_trashed` is `true` when this page's own parent is still in\n  the Trash. The page is live again but has no visible place in the tree\n  until that parent is restored too — surface it, or the user hunts for a\n  wiki that \"came back\" and then isn't anywhere. `message` already says so\n  in prose.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Wiki restored from Trash",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "wiki_id",
                    "restored",
                    "sub_wikis_restored",
                    "parent_still_trashed",
                    "message"
                  ],
                  "properties": {
                    "wiki_id": {
                      "type": "integer",
                      "description": "The id of the restored wiki.",
                      "example": 2633
                    },
                    "restored": {
                      "type": "boolean",
                      "description": "Always true on a 200 (the wiki left the Trash).",
                      "example": true
                    },
                    "sub_wikis_restored": {
                      "type": "integer",
                      "description": "How many descendant sub-wikis came back with it — the ones this page's own delete cascaded. Excludes descendants that were trashed separately and stay in the Trash.",
                      "example": 2
                    },
                    "parent_still_trashed": {
                      "type": "boolean",
                      "description": "True when this wiki's parent is still in the Trash, so the restored page won't appear in the tree until that parent is restored too. Always false for a top-level wiki.",
                      "example": false
                    },
                    "message": {
                      "type": "string",
                      "description": "The web's own restore wording, safe to show verbatim.",
                      "example": "Restored \"Dental\". 2 sub-wikis restored with it."
                    },
                    "_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 trashed wiki with that id is restorable by this caller — missing, another tenant's, or created by someone else and the caller is not a Wikis app admin (error code `not_found`)."
          },
          "409": {
            "description": "The wiki exists and the caller may restore it, but it is **not in the Trash** — it was never deleted, or a duplicate/retried call already restored it (error code `not_discarded`). Nothing changed; treat it as already restored."
          }
        }
      }
    },
    "/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`), or the token lacks `read:wikis` (error code `insufficient_permissions`)."
          },
          "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 viewers in the named list (honors search; people a works council hides are counted in names_hidden instead).",
                          "example": 12
                        },
                        "current_page": {
                          "type": "integer",
                          "example": 1
                        },
                        "total_pages": {
                          "type": "integer",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "example": 20
                        }
                      }
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present only when a works council hides names (General\nSettings › Countries): the viewers left out of `viewers`\n— counted, never named.\nWhile the hidden group is under the company's smallest reportable\ngroup, `count` and `group_count` are null and `fewer_than` is that\ngroup, \"a few\" (the status counts beside it count only the named\npeople) — the shape of `BroadcastNamesHidden`.\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "group_count": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                        },
                        "fewer_than": {
                          "type": "integer",
                          "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    },
                    "_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 who\ncan read the wiki. Mentioned people who cannot read it are not\nnotified; the response lists them in `mentions_not_notified` so the\nclient can tell the author.\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"
                    },
                    "mentions_not_notified": {
                      "type": "array",
                      "description": "Mentioned people who were not notified because they cannot read the wiki (empty when everyone mentioned can read it, while the tenant has mention notifications switched off, or if sending them failed).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "mention_notices_off": {
                      "type": "boolean",
                      "description": "True when the comment @mentions someone besides its author while the tenant has \"Notify Users on @Mention\" switched off in Wikis Settings — nobody was told they were mentioned (`mentions_not_notified` is then empty). The web and phone readers show the author a warning."
                    }
                  }
                }
              }
            }
          },
          "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 company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed).\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 are the company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed), plus one the\ncaller already holds from outside it (so it can be removed); any other\nvalue returns `422 invalid_emoji` with the `allowed` list.\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. The token asks `write:wikis`, as every change here does.\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 emoji from the company's reaction palette.",
                    "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. Any caller with Wikis-app\n  access may bookmark; the token asks `write:wikis`.\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`). An archived wiki drops out of the default\nbrowse tree and can be unarchived. Archive is a status; deleting a wiki\nis DELETE /wikis/{id}, and POST /wikis/{id}/restore brings it back.\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  the token asks `destructive:wikis` (below).\n* **A wiki someone else is actively editing answers `409`**, not `403` —\n  the lock is transient, so retrying after it clears succeeds.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:wikis` scope.\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; the token asks\n  `write:wikis` — unarchiving restores, so it is not delete-level.\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 are the company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed), plus one the\ncaller already holds from outside it (so it can be removed); any other\nvalue returns `422 invalid_emoji` with the `allowed` list.\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. The token asks `write:wikis`.\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 emoji from the company's reaction palette.",
                    "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 company's reaction palette (its admin's choice from the one reaction catalog, the whole catalog unless narrowed).\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"
                    },
                    "mentions_not_notified": {
                      "type": "array",
                      "description": "People this edit newly mentioned who were not notified because they cannot read the wiki (empty when everyone mentioned can read it, while the tenant has mention notifications switched off, or if sending them failed).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "mention_notices_off": {
                      "type": "boolean",
                      "description": "True when this edit added an @mention of someone besides its author while the tenant has \"Notify Users on @Mention\" switched off in Wikis Settings — nobody was told they were mentioned (`mentions_not_notified` is then empty). The web and phone readers show the author a warning."
                    }
                  }
                }
              }
            }
          },
          "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 comment's **author within 5 minutes** of posting; the **page's\nmanager**, a **business admin** or a **Wikis app admin** at any time;\notherwise 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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:wikis` scope.\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 its author (nor the page's manager or an admin), or past the author's 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  Both are also `false` while another user is editing the page; `can_delete`\n  too while someone is editing one of its sub-wikis (DELETE answers `409`\n  `wiki_being_edited`).\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 person who manages the page 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",
                        "description",
                        "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"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true,
                          "description": "The page's lead (Properties → Description, up to 500 characters), which the web reader prints under the title. Plain text; null when the page has none. Not part of the `web_view_url` body — draw it in the native screen.",
                          "example": "How we run and monitor the coordination cluster."
                        },
                        "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). While a works\ncouncil hides fewer of the page's readers than the company's\nsmallest reportable group, the views of the named viewers\nonly — the count every door shows (Knowledge Layer X3).\n",
                          "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"
                            }
                          }
                        },
                        "owner": {
                          "type": "object",
                          "nullable": true,
                          "description": "The page's named owner (Properties › Owner) — who answers for keeping it accurate, not who manages it (`creator`). Null when none is named or that person is no longer an active member.",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 455
                            },
                            "name": {
                              "type": "string",
                              "example": "Priya Shah"
                            }
                          }
                        },
                        "owner_team": {
                          "type": "object",
                          "nullable": true,
                          "description": "The owning team (Properties › Owner), null when none or no longer active.",
                          "required": [
                            "id",
                            "name"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 31
                            },
                            "name": {
                              "type": "string",
                              "example": "People Ops"
                            }
                          }
                        },
                        "owned_by": {
                          "type": "string",
                          "nullable": true,
                          "description": "The owner line readers see: the owner and/or team, else the person who manages the page standing in, written \"Name (manages it)\". An ownership fact, not the byline, so it is reported whatever `show_author` says.",
                          "example": "Priya Shah"
                        },
                        "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, and nobody else is editing it or one of the sub-wikis it would take to Trash.",
                          "example": true
                        },
                        "can_share": {
                          "type": "boolean",
                          "description": "May the caller share this wiki (POST /shares): anyone who can open it, with a `write:shares` token.",
                          "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\nDelete-level (owner decision 2026-10-04, #6659): requires the `destructive:wikis`\nscope.\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`)."
          },
          "409": {
            "description": "Another user is editing this wiki, or one of the sub-wikis the delete would move to Trash with it (error code `wiki_being_edited`); the message names the page and its editor when the caller can open that page. Distinct from `403` because the condition is transient — retry once they are done. `can_delete` on the detail payload is false while this applies."
          }
        }
      }
    },
    "/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 compose and stage emergency alerts — manager+, mirroring\nthe web authorize_alert_access gate — with a credential holding\nwrite:broadcasts in full (an own-only or read-only token gets\nfalse). Cancel is `can_cancel`, Send is `can_dispatch`, and the\nTracking rosters are each alert's `can_view_tracking`.\n"
                    },
                    "can_cancel": {
                      "type": "boolean",
                      "description": "Whether the caller may stand an alert down — cancel it, discard a draft or withdraw its approval request. `can_manage` AND a credential holding destructive:safety_hub, which those doors ask (a signed-in session always does)."
                    },
                    "can_dispatch": {
                      "type": "boolean",
                      "description": "Whether the caller may DISPATCH an alert: `can_cancel` AND the tenant's per-tenant dispatch policy (who_can_dispatch_alerts)."
                    },
                    "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** (see **Approval** 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:** an emergency alert is never held for approval. When an\nENFORCED approval workflow covers emergency alerts and the caller is not an\nadmin, a *publish* still dispatches (`status: \"sending\"`); the override\nis recorded on the alert's approval request (status `overridden`) and the\nworkflow's reviewers are told afterwards. Same as the web Send-Now. To ask\nfor a review BEFORE sending, save a draft and call\n`POST /alerts/{id}/submit_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\nPublishing (`alert[status]` = `publish`) sends the alert, which is\ndelete-level (owner decision 2026-10-04): that request also requires\n`destructive:safety_hub`. A draft needs only `write:broadcasts`.\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 — an approval workflow never\nholds an alert). 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 by reference: ActiveStorage blob\n`signed_id`s the client already holds. Prefer\n`media_files`. The generic\n`POST /rails/active_storage/direct_uploads` endpoint is\ndisabled (403) and is not a way to obtain one. Subject\nto the same media rules; any invalid or expired\nreference fails the whole call with 422.\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"
                    }
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The SAME body, form-encoded, plus the attachment file parts. A file can only be sent this way — `format: binary` inside a JSON-only requestBody is something no generated client (and no Swagger UI) can actually post. Every other field is sent with its bracketed name, e.g. `alert[title]`, `alert[body]`, `alert[status]`, `notification_recipient_group_ids[]`.",
                "properties": {
                  "alert[media_files][]": {
                    "type": "array",
                    "maxItems": 4,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The attachment file parts, uploaded in THIS request. Works with every authentication method, Bearer tokens included. They land in the alert's `media_files` (Drive) — the same attachments the web composer and the show API expose. Validated server-side against the alert media rules (photos/video only, per-file size cap, max 4 files, counted together with `media_signed_ids`); any invalid file fails the whole call with 422 `invalid_attachments` and nothing is persisted."
                  },
                  "alert[media_signed_ids][]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Attachments by reference — see the JSON body's `media_signed_ids`."
                  },
                  "alert[title]": {
                    "type": "string"
                  },
                  "alert[body]": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "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**: **dispatched** immediately\n(`status: \"sending\"`). An enforced approval workflow covering\nemergency alerts never holds it; the send is recorded as an override\nand the workflow's reviewers are told afterwards.\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_files / 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"
                      ],
                      "description": "`draft` (saved only) or `sending` (dispatched — an\napproval workflow never holds an alert).\n"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Manager access required, or app not enabled"
          },
          "422": {
            "description": "Validation failed, or the alert cannot be dispatched as written (`dispatch_failed`)"
          }
        }
      }
    },
    "/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 compose and stage emergency alerts (same rule as GET /alerts)."
                    },
                    "can_cancel": {
                      "type": "boolean",
                      "description": "Whether the caller may stand an alert down — cancel it, discard a draft or withdraw its approval request. `can_manage` AND a credential holding destructive:safety_hub, which those doors ask (a signed-in session always does)."
                    },
                    "can_dispatch": {
                      "type": "boolean",
                      "description": "Whether the caller may DISPATCH an alert — `can_cancel` AND 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": "The root `can_manage` of GET /alerts — whether the caller may compose and stage alerts — NOT this alert's tracking gate: draw the \"Tracking Details\" affordance from `can_view_tracking`, which matches GET /alerts/{id}/acknowledgements, /check_ins and /recipients.\n"
                            },
                            "can_cancel": {
                              "type": "boolean",
                              "description": "The root `can_cancel` of GET /alerts."
                            },
                            "can_dispatch": {
                              "type": "boolean",
                              "description": "The root `can_dispatch` of GET /alerts."
                            },
                            "can_remind_unresponded": {
                              "type": "boolean",
                              "description": "Sender's side only. True when POST /alerts/{id}/remind_unresponded would send now — the caller manages alerts, tracks this one, may send alerts, holds write:broadcasts and destructive:safety_hub, and nothing refuses it (the 15-minute wait included).\n"
                            },
                            "remind_unresponded_refusal": {
                              "type": "string",
                              "nullable": true,
                              "description": "Why Remind is not offered right now (e.g. a reminder went out less than 15 minutes ago), for a disabled button with its reason; null when it is offered or the caller could not press it anyway.\n"
                            },
                            "screens": {
                              "type": "object",
                              "nullable": true,
                              "description": "Whether Digital Signage break-room screens carry this alert — the web alert page's \"Break-room screens\" row, from the rule the screens play by. Returned to the sender's side only (the same gate as `audience`); `null` for a recipient, for an alert that never went out, or for a business with no active screen.\n",
                              "properties": {
                                "state": {
                                  "type": "string",
                                  "enum": [
                                    "showing",
                                    "no_screen_now",
                                    "not_for_everyone",
                                    "sample",
                                    "stood_down",
                                    "ended",
                                    "displaced"
                                  ],
                                  "description": "`showing` — on at least one screen now; `no_screen_now` — still eligible, but every screen has Critical alerts off or out of schedule, or a full Max items; `not_for_everyone` — screens show only alerts sent to everyone; `sample` — sample alerts never play; `stood_down`; `ended` — its time on screens is over; `displaced` — newer alerts hold the screens' places for now (it comes back if they leave before `until`).\n"
                                },
                                "screens": {
                                  "type": "integer",
                                  "description": "Active screens in the business."
                                },
                                "showing_on": {
                                  "type": "integer",
                                  "description": "Screens showing it now (0 unless `showing`)."
                                },
                                "showing_on_sample": {
                                  "type": "integer",
                                  "description": "How many of `showing_on` are sample screens (from \"Load sample data\") — they really play the business's alerts."
                                },
                                "critical_alerts_off": {
                                  "type": "integer",
                                  "description": "Screens where alerts are switched off right now — the screen's Critical alerts setting, its schedule, or the app's default (counted for `showing` and `no_screen_now`; 0 otherwise)."
                                },
                                "max_items_full": {
                                  "type": "integer",
                                  "description": "Screens whose Max items other alerts already fill — urgent alerts of the last 24 hours first, then the newest (counted for `showing` and `no_screen_now`; 0 otherwise)."
                                },
                                "showing_on_not_checked_in": {
                                  "type": "integer",
                                  "description": "How many of `showing_on` have not checked in within the online window (the screen's last check-in is older than 5 minutes), or whose display URL was replaced with no device on the new one yet — they show it once they check in, until `until`."
                                },
                                "until": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When screens let it go — set for `showing`, `no_screen_now`, `displaced` (how long it can come back) and `ended` (in the past once `ended`); `null` in the other states, which its clock does not govern.\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_files][]` / `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_files][]` and `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 — an approval workflow never holds an alert) —\nthe response then carries `status: \"sending\"`; any other value (incl.\nabsent) 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`) —\nexcept a publish-only call (`alert[status]` = `publish` and no other\nfield), which sends the alert as it is, like `send_now`.\nRequires **manager or above**.\n\nPublishing (`alert[status]` = `publish`) sends the alert, which is\ndelete-level (owner decision 2026-10-04): that request also requires\n`destructive:safety_hub`. A draft needs only `write:broadcasts`.\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": "ActiveStorage blob signed_ids the client already holds, to ADD to the draft. Prefer the multipart file parts."
                      }
                    }
                  },
                  "notification_recipient_group_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Notification recipient group ids (top-level; replaces the draft's groups when sent)."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "The same edit, form-encoded, plus the attachment file parts — the only content type a file can actually be sent on. Every other field takes its bracketed name, e.g. `alert[title]`, `alert[status]`.",
                "properties": {
                  "alert[media_files][]": {
                    "type": "array",
                    "maxItems": 4,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "File parts to ADD to the draft. Counted with the files the draft already holds against the 4-file limit; an invalid file fails the edit with 422 `invalid_attachments` and nothing is changed."
                  },
                  "alert[media_signed_ids][]": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Attachments by reference to ADD to the draft. Prefer the file parts."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft edited. `status: \"draft\"` when kept as a draft; `\"sending\"` when\npublished (dispatched — an enforced approval workflow records the send\nas an override rather than holding it).\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"
                      ]
                    },
                    "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 before an edit (`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",
                            "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\nDelete-level (owner decision 2026-10-04): requires the `destructive:safety_hub`\nscope.\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. An emergency alert is never held for approval: when an enforced approval workflow covers emergency alerts and the caller is not an admin, the alert still dispatches, the override is recorded on its approval request (an open request closes as `overridden`) and the workflow's reviewers are told afterwards.\nDelete-level (owner decision 2026-10-04): requires the `destructive:safety_hub` scope.\n",
        "responses": {
          "200": {
            "description": "Alert enqueued"
          },
          "403": {
            "description": "Manager access required, or the alert is already dispatched"
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "The alert cannot be dispatched as written (error code `dispatch_failed`, e.g. template placeholders still unfilled).\n"
          }
        }
      }
    },
    "/alerts/{id}/remind_unresponded": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Alert ID",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remind everyone who has not checked in (the muster board's Remind)",
        "description": "Re-sends a live safety check-in to everyone who has not answered, as the\nweb muster board's \"Remind who hasn't answered\" does. Only a manager or\nSafety Hub app admin who is the alert's sender or an alert admin and may\nalso send alerts (the tenant's \"who can send alerts\" rule), on a token\nthat may write alerts (`write:broadcasts`) and Send and Cancel\n(`destructive:safety_hub`). The model's refusals apply, checked again\nunder the row lock: the alert must be live and asking for check-ins,\nthere must be a reminder channel, and at most one reminder every 15\nminutes. The alert detail's `can_remind_unresponded` /\n`remind_unresponded_refusal` say beforehand whether it would be sent.\n",
        "responses": {
          "200": {
            "description": "Reminder queued (or nobody left to remind — `reminded_count` 0).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "reminded_count": {
                      "type": "integer",
                      "example": 4
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "in_app",
                        "push",
                        "sms"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "Reminding 4 people who have not checked in — in the app, by push, and by text."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a manager or Safety Hub app admin, not the alert's sender or an alert admin, not allowed to send alerts, or the token lacks write:broadcasts or destructive:safety_hub."
          },
          "404": {
            "description": "Alert not found"
          },
          "422": {
            "description": "Refused (`remind_refused`) — the message is the reason, e.g. a reminder went out less than 15 minutes ago."
          }
        }
      }
    },
    "/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, sent or completed alert (manager and above)",
        "description": "Delete-level (owner decision 2026-10-04): requires the `destructive:safety_hub`\nscope.\n",
        "responses": {
          "200": {
            "description": "Alert cancelled"
          },
          "403": {
            "description": "Manager 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.\nDelete-level (owner decision 2026-10-04): requires the `destructive:safety_hub` scope.\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 — and\n`meta.channel_delivery_counts` is the same roster split by delivery\noutcome (`sent`, `skipped`, `failed`, `pending`, every key always\npresent) for a \"N sent · 0 skipped · 0 failed · 0 pending\" line. Both\nare roster-wide aggregates returned **only on page 1** (the client\nrenders them once in the header); they are 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
                                },
                                "fallback_language": {
                                  "type": "string",
                                  "nullable": true,
                                  "description": "The recipient's language this channel went out WITHOUT — it went out in the original language because the alert's translation into it was not ready yet, or translation is turned off. Null when it went out in the recipient's language, and always null for email (written in the recipient's language when it is sent)."
                                },
                                "translated_followup_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "When this text or mobile-app push was sent again in the recipient's language, because the translation was ready within 5 minutes of Send. Null when no follow-up went out."
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "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"
                          }
                        },
                        "channel_delivery_counts": {
                          "type": "object",
                          "description": "Distinct recipients per channel split by delivery outcome, keyed\nby channel name. Every status key is present (0 when none), so a\nclient can print \"N sent · 0 skipped · 0 failed · 0 pending\".\nReturned **only on page 1**; omitted on subsequent pages.\n",
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "sent": {
                                "type": "integer"
                              },
                              "skipped": {
                                "type": "integer"
                              },
                              "failed": {
                                "type": "integer"
                              },
                              "pending": {
                                "type": "integer"
                              }
                            },
                            "required": [
                              "sent",
                              "skipped",
                              "failed",
                              "pending"
                            ]
                          }
                        },
                        "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"
          }
        }
      }
    },
    "/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,
                          "description": "The next compensation review date your organization has set; null when none is set.",
                          "example": "2024-12-01"
                        },
                        "last_updated": {
                          "type": "object",
                          "description": "The most recent compensation change on record; both fields are null when there is none.",
                          "properties": {
                            "date": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "When the change was recorded.",
                              "example": "2024-01-15T10:30:00Z"
                            },
                            "by": {
                              "type": "string",
                              "nullable": true,
                              "description": "Who made the change, or \"System\" for a change recorded with no author (an automated step increase or import).",
                              "example": "Jane Smith"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "total_changes": {
                                  "type": "integer",
                                  "description": "Compensation changes matching the date filters (same as `total_count`)."
                                },
                                "date_range": {
                                  "type": "object",
                                  "description": "The `from_date` / `to_date` filters as sent (null when absent).",
                                  "properties": {
                                    "from": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "2026-01-01"
                                    },
                                    "to": {
                                      "type": "string",
                                      "nullable": true
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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",
                          "description": "Not tracked yet — both fields are always null.",
                          "properties": {
                            "has_recent_review": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Not tracked yet; always null.",
                              "example": null
                            },
                            "merit_increase_eligible": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Not tracked yet; always null.",
                              "example": null
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "statistics": {
                                  "type": "object",
                                  "description": "The caller's own requests by approval status, across every page and ignoring the `status` filter.",
                                  "properties": {
                                    "total": {
                                      "type": "integer"
                                    },
                                    "pending": {
                                      "type": "integer"
                                    },
                                    "approved": {
                                      "type": "integer"
                                    },
                                    "rejected": {
                                      "type": "integer"
                                    },
                                    "cancelled": {
                                      "type": "integer"
                                    }
                                  }
                                },
                                "can_create_new": {
                                  "type": "boolean",
                                  "description": "Whether the caller may submit a new compensation change request now."
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      },
      "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:compensation` scope.\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,
                                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                              }
                            }
                          },
                          "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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/{goal_id}/documents/{id}": {
      "delete": {
        "tags": [
          "EPMS Goals"
        ],
        "summary": "Remove a document from a goal",
        "description": "Removes one document attached to a goal. The file itself is purged in the\nbackground after the response.\n\nNeeds the Goals Management module (403 `module_not_enabled` when it is off)\nand a caller who may manage the goal's documents (403 `forbidden`, \"You do\nnot have permission to manage documents on this goal\").\n\n**Required Scopes:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope; `write:epms_goals` is not enough. A token\nwithout it is refused with 403 `insufficient_permissions` (\"Required\nscope: destructive:epms\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "goal_id",
            "in": "path",
            "required": true,
            "description": "Goal ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The document's attachment ID (as listed in the goal's attachments)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Document removed"
          },
          "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` — \"Goal not found\" (no such goal in this business) or\n\"Attachment not found\" (no such document on this goal).\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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 234
                          },
                          "title": {
                            "type": "string",
                            "example": "Q1 2026 Performance Review"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "created",
                              "in_progress",
                              "completed",
                              "approved",
                              "archived"
                            ],
                            "example": "in_progress"
                          },
                          "display_status": {
                            "type": "string",
                            "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                            "enum": [
                              "created",
                              "in_progress",
                              "completed",
                              "approved",
                              "archived",
                              "overdue"
                            ],
                            "example": "in_progress"
                          },
                          "status_color": {
                            "type": "string",
                            "description": "Bootstrap badge color variant for the display status",
                            "enum": [
                              "info",
                              "primary",
                              "warning",
                              "success",
                              "danger",
                              "secondary"
                            ],
                            "example": "primary"
                          },
                          "display_status_label": {
                            "type": "string",
                            "description": "The display status in words, as the web list labels it.",
                            "example": "In Progress"
                          },
                          "review_type": {
                            "type": "string",
                            "enum": [
                              "annual",
                              "biannual",
                              "quarterly",
                              "probationary",
                              "ad_hoc"
                            ],
                            "example": "quarterly"
                          },
                          "review_period": {
                            "type": "string",
                            "description": "The review period in words.",
                            "example": "Jan 2026 - Mar 2026"
                          },
                          "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",
                            "nullable": true,
                            "example": "2026-04-15"
                          },
                          "overall_rating": {
                            "type": "number",
                            "nullable": true,
                            "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                            "example": 4.5
                          },
                          "overall_rating_display": {
                            "type": "string",
                            "nullable": true,
                            "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                            "example": "4.5 / 5"
                          },
                          "rating_scale": {
                            "type": "integer",
                            "example": 5
                          },
                          "completion_percentage": {
                            "type": "number",
                            "example": 60
                          },
                          "is_overdue": {
                            "type": "boolean",
                            "example": false
                          },
                          "days_until_due": {
                            "type": "integer",
                            "nullable": true,
                            "example": 12
                          },
                          "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"
                          },
                          "employee": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 456
                              },
                              "name": {
                                "type": "string",
                                "example": "John Doe"
                              },
                              "email": {
                                "type": "string",
                                "nullable": true,
                                "example": "john.doe@example.com"
                              },
                              "job_title": {
                                "type": "string",
                                "nullable": true,
                                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                                "example": "Software Engineer"
                              }
                            }
                          },
                          "reviewer": {
                            "type": "object",
                            "description": "The reviewer; absent when the review has none.",
                            "properties": {
                              "id": {
                                "type": "integer",
                                "example": 789
                              },
                              "name": {
                                "type": "string",
                                "example": "Jane Manager"
                              },
                              "email": {
                                "type": "string",
                                "nullable": true,
                                "example": "jane.manager@example.com"
                              }
                            }
                          },
                          "manager_comments": {
                            "type": "string",
                            "nullable": true,
                            "description": "Detail only. Null when the caller may not see the manager's work.",
                            "example": "Great progress this quarter"
                          },
                          "employee_self_assessment": {
                            "type": "string",
                            "nullable": true,
                            "description": "Detail only."
                          },
                          "goals_count": {
                            "type": "integer",
                            "description": "Detail only."
                          },
                          "completed_goals_count": {
                            "type": "integer",
                            "description": "Detail only."
                          },
                          "goals_completion_percentage": {
                            "type": "number",
                            "description": "Detail only."
                          },
                          "can_edit": {
                            "type": "boolean",
                            "description": "Detail only."
                          },
                          "can_submit": {
                            "type": "boolean",
                            "description": "Detail only."
                          },
                          "can_approve": {
                            "type": "boolean",
                            "description": "Detail only."
                          },
                          "self_assessment_required": {
                            "type": "boolean",
                            "description": "Detail only."
                          },
                          "employee_submitted_self_assessment": {
                            "type": "boolean",
                            "description": "Detail only."
                          },
                          "meeting_completed": {
                            "type": "boolean",
                            "description": "Detail 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": "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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\nWhen the business has manager approval switched off, the submission IS\nthe finish (as on desktop and phone): the review is also approved and\nfinalized in the same call, so `performance_review` comes back approved.\nIf that finalization fails the call answers 422 `approve_failed`\n(\"The review was submitted but could not be finalized: …\").\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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    },
                    "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\nRuns the same finalization as desktop, phone and bulk approve (merit\nrecommendation, outcome automation, post-review survey, completion mail,\naudit). Refused 403 `forbidden` for the employee on the review AND for\nthe reviewer who submitted it for approval — someone else approves it\n(their manager, an admin or an EPMS app admin). A failed finalization is\n422 `approve_failed` with its reason.\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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    },
                    "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    },
                    "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 234
                        },
                        "title": {
                          "type": "string",
                          "example": "Q1 2026 Performance Review"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived"
                          ],
                          "example": "in_progress"
                        },
                        "display_status": {
                          "type": "string",
                          "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
                          "enum": [
                            "created",
                            "in_progress",
                            "completed",
                            "approved",
                            "archived",
                            "overdue"
                          ],
                          "example": "in_progress"
                        },
                        "status_color": {
                          "type": "string",
                          "description": "Bootstrap badge color variant for the display status",
                          "enum": [
                            "info",
                            "primary",
                            "warning",
                            "success",
                            "danger",
                            "secondary"
                          ],
                          "example": "primary"
                        },
                        "display_status_label": {
                          "type": "string",
                          "description": "The display status in words, as the web list labels it.",
                          "example": "In Progress"
                        },
                        "review_type": {
                          "type": "string",
                          "enum": [
                            "annual",
                            "biannual",
                            "quarterly",
                            "probationary",
                            "ad_hoc"
                          ],
                          "example": "quarterly"
                        },
                        "review_period": {
                          "type": "string",
                          "description": "The review period in words.",
                          "example": "Jan 2026 - Mar 2026"
                        },
                        "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",
                          "nullable": true,
                          "example": "2026-04-15"
                        },
                        "overall_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
                          "example": 4.5
                        },
                        "overall_rating_display": {
                          "type": "string",
                          "nullable": true,
                          "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
                          "example": "4.5 / 5"
                        },
                        "rating_scale": {
                          "type": "integer",
                          "example": 5
                        },
                        "completion_percentage": {
                          "type": "number",
                          "example": 60
                        },
                        "is_overdue": {
                          "type": "boolean",
                          "example": false
                        },
                        "days_until_due": {
                          "type": "integer",
                          "nullable": true,
                          "example": 12
                        },
                        "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"
                        },
                        "employee": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 456
                            },
                            "name": {
                              "type": "string",
                              "example": "John Doe"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "john.doe@example.com"
                            },
                            "job_title": {
                              "type": "string",
                              "nullable": true,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "example": "Software Engineer"
                            }
                          }
                        },
                        "reviewer": {
                          "type": "object",
                          "description": "The reviewer; absent when the review has none.",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "example": 789
                            },
                            "name": {
                              "type": "string",
                              "example": "Jane Manager"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "example": "jane.manager@example.com"
                            }
                          }
                        },
                        "manager_comments": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only. Null when the caller may not see the manager's work.",
                          "example": "Great progress this quarter"
                        },
                        "employee_self_assessment": {
                          "type": "string",
                          "nullable": true,
                          "description": "Detail only."
                        },
                        "goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "completed_goals_count": {
                          "type": "integer",
                          "description": "Detail only."
                        },
                        "goals_completion_percentage": {
                          "type": "number",
                          "description": "Detail only."
                        },
                        "can_edit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_submit": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "can_approve": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "self_assessment_required": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "employee_submitted_self_assessment": {
                          "type": "boolean",
                          "description": "Detail only."
                        },
                        "meeting_completed": {
                          "type": "boolean",
                          "description": "Detail only."
                        }
                      }
                    },
                    "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,
                                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                                "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,
                                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                                "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                              "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 where the People directory shows the caller email (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": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own 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:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\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,
                              "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
                            }
                          }
                        },
                        "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": "Permanently deletes a CANCELLED meeting. A scheduled or in-progress\nmeeting must be cancelled first (`POST /epms/meetings/{id}/cancel`,\nwhich notifies the attendees); a completed meeting is a record and\ncannot be cancelled or deleted. Any other state answers 422\n`not_deletable`.\n\nAllowed for the people who may change the meeting: its organizer,\nwhoever manages the employee, and whoever holds authority over its\nlinked performance review.\n\n**Required Scopes:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Meeting deleted successfully"
          },
          "422": {
            "description": "The meeting is not cancelled, so it cannot be deleted (`not_deletable`)"
          },
          "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}/cancel": {
      "post": {
        "tags": [
          "EPMS Meetings"
        ],
        "summary": "Cancel meeting",
        "description": "Moves a meeting to `cancelled` and notifies its attendees. The meeting\nstays on record and can then be permanently deleted with\n`DELETE /epms/meetings/{id}`. A completed or already-cancelled meeting\nanswers 422 `not_cancellable`.\n\nAllowed for the people who may change the meeting: its organizer,\nwhoever manages the employee, and whoever holds authority over its\nlinked performance review.\n\n**Required Scopes:** `destructive:epms`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:epms` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Meeting ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why the meeting is cancelled; included in the attendees' notice"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meeting cancelled",
            "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": "The meeting is completed or already cancelled (`not_cancellable`), or could not be saved (`cancel_failed`)"
          },
          "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. Anyone who may open\nthe meeting may complete it — the employee included. A completed or\ncancelled meeting answers 422.\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` — and, on top of it,\n`destructive:epms` for `status: \"cancelled\"` on a draft or in-progress\nassessment, which retires it for good (owner decision 2026-10-07, #7182).\n\nAn edit never approves: `status: \"approved\"` is refused with\n`422 not_approvable` — an assessment is approved by its approver once it\nis completed (#7249). Complete it with `POST …/submit` or\n`status: \"completed\"`. `PATCH` behaves identically.\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": "The edit was refused:\n\n* a field failed validation — the body is the standard validation\n  error list;\n* `not_approvable` — the edit set `status: \"approved\"`. An assessment\n  is approved by its approver once it is completed, not by an edit\n  (#7249).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "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"
                              },
                              "code": {
                                "type": "string",
                                "description": "Always `validation_error`"
                              },
                              "field_name": {
                                "type": "string",
                                "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "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": {
                  "not_approvable": {
                    "value": {
                      "error": {
                        "code": "not_approvable",
                        "message": "An assessment is approved by its approver once it is completed, not by an edit."
                      }
                    }
                  }
                }
              }
            }
          },
          "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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 leave request that is awaiting approval, or approved leave that\nhas not started yet. A request that starts today or earlier, or whose\nschedule period has closed, cannot be modified (403 request_not_editable).\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 that is still awaiting approval and has not\nstarted yet (its schedule period still open). Anything else is refused\nwith 403 request_not_deletable — cancel it instead.\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:leave_management`\nscope. A personal token cannot hold it: do this in MangoApps instead.\n",
        "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.\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:leave_management`\nscope. A personal token cannot hold it: do this in MangoApps instead.\n",
        "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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,
                                          "description": "Who made the change; null for an automatic change (see `automatic`).",
                                          "properties": {
                                            "id": {
                                              "type": "integer",
                                              "example": 2
                                            },
                                            "name": {
                                              "type": "string",
                                              "example": "Manager Name"
                                            }
                                          }
                                        },
                                        "automatic": {
                                          "type": "boolean",
                                          "description": "True for a change no person made — hours recounted after a holiday or working-day change, or approved leave cancelled when the employee left.",
                                          "example": false
                                        },
                                        "notes": {
                                          "type": "string",
                                          "nullable": true,
                                          "example": "Approved for vacation"
                                        },
                                        "changed_at": {
                                          "type": "string",
                                          "format": "date-time",
                                          "example": "2024-02-16T14:30:00Z"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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
                        },
                        "special_approval_requests": {
                          "type": "integer",
                          "example": 1
                        },
                        "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
                          },
                          "special_approval_requests": {
                            "type": "integer",
                            "example": 1
                          },
                          "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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
                                  }
                                }
                              },
                              "source": {
                                "$ref": "#/components/schemas/ManagedSource"
                              }
                            },
                            "required": [
                              "leave_type_id",
                              "leave_type",
                              "year",
                              "accrued",
                              "used",
                              "balance",
                              "pending",
                              "percentages"
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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). Hour figures are strings with two decimals.",
                                "properties": {
                                  "accrued": {
                                    "type": "number",
                                    "example": 15.0
                                  },
                                  "used": {
                                    "type": "number",
                                    "example": 5.0
                                  },
                                  "balance": {
                                    "type": "number",
                                    "example": 10.0
                                  },
                                  "balance_hours": {
                                    "type": "string",
                                    "description": "The ledger balance (accrued − used). It still counts hours waiting for a decision — show available_to_request_hours as what a new request can use.",
                                    "example": "10.00"
                                  },
                                  "pending_hours": {
                                    "type": "string",
                                    "description": "Hours in this type's requests that are waiting for a decision.",
                                    "example": "4.00"
                                  },
                                  "available_to_request_hours": {
                                    "type": "string",
                                    "description": "Hours a new request can draw today — the ledger balance less pending requests, the figure the server books a request against (the request form's \"hours left to request\").",
                                    "example": "6.00"
                                  },
                                  "year": {
                                    "type": "integer",
                                    "example": 2024
                                  }
                                }
                              }
                            },
                            "required": [
                              "id",
                              "name",
                              "color",
                              "icon",
                              "active",
                              "default"
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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": "List holidays, with optional filtering by year, date range, or upcoming holidays.\nWith `location_id`, the holidays that apply to that location: its own, its\nparent location's, its jurisdictions' (with their parents, e.g. state and\ncountry) and company-wide ones. Without it, an admin or an integration (a\nservice account) reads every holiday in the business; anyone else reads their\nown — those that apply to their location, in the same way. `scope=mine` asks\nfor the caller's own holidays whoever they are, admins included.\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
            }
          },
          {
            "name": "location_id",
            "in": "query",
            "description": "The holidays that apply to that location — its own, its parent's, its jurisdictions' and company-wide ones. 404 `location_not_found` when the location is not in this business.",
            "schema": {
              "type": "integer",
              "example": 12
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Only holidays of this kind.",
            "schema": {
              "type": "string",
              "enum": [
                "business",
                "jurisdiction",
                "location"
              ]
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "`mine` — the caller's own holidays, for every caller, admins included: those that apply to their primary location (else their default location), else the company-wide ones when they have neither. The same answer a member's call without it gives. Ignored when `location_id` is given; any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "mine"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Holidays retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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"
                            ]
                          }
                        },
                        "returned_count": {
                          "type": "integer",
                          "description": "Rows in `items`. At most 500 holidays are returned; `total_count` is the true number in the window."
                        },
                        "truncated": {
                          "type": "boolean",
                          "description": "True when the window holds more than the 500 rows returned (`total_count` > `returned_count`)."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "`invalid_date_range` — `start_date` or `end_date` is not a date.",
            "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": "`location_not_found` — `location_id` is not a location 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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/holidays/year/{year}": {
      "get": {
        "tags": [
          "Leave Management"
        ],
        "summary": "Get holidays for specific year",
        "description": "A year's holidays. An admin or an integration (a service account) reads every\nholiday in the business; anyone else reads those that apply to their location\n— its own, its jurisdictions' and the company's. `scope=mine` asks for the\ncaller's own holidays whoever they are, admins included.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "path",
            "required": true,
            "description": "Year to get holidays for",
            "schema": {
              "type": "integer",
              "example": 2024
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "`mine` — the caller's own holidays, for every caller, admins included: those that apply to their primary location (else their default location), else the company-wide ones. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "mine"
              ]
            }
          }
        ],
        "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
                    },
                    "returned_count": {
                      "type": "integer",
                      "description": "Rows in `holidays`. At most 500 are returned; `total_count` is the true number in the year."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the year holds more than the 500 rows returned."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_year` — the year is outside 1900–2100.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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"
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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
            }
          },
          {
            "name": "exclude_request_id",
            "in": "query",
            "description": "Your own leave request being edited; its days are not counted as taken",
            "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. Each day it fills has its existing availability replaced; the days\nit skips keep theirs. The requested window is kept, rounded inward to whole hours,\nand split into blocks of the business's minimum block size from its start; a\nremainder shorter than one block is not created.\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\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/skills/trending": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get trending skills",
        "description": "Get skills that have been added frequently by employees recently\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\nnames and descriptions (falling back to platform defaults). Use this to\npopulate a proficiency picker without hardcoding labels — the integer\n`level` is what you submit to the employee_skills endpoints.\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "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\nEach item says whether skill detection added it (`detected`, `source`) and, on the\ncaller's own holdings, `disputed` and `not_mine_available`.\n`meta.skill_detection_notice` is `{ shown, text }`: the phone list's explanation of\nautomatic skill detection (text null and shown false when detection is off and the\ncaller holds no detected skill).\n\n**Use Cases:**\n- View employee's skill profile\n- Filter skills by category or status\n- Check certification expiration dates\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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. \"ended\" lists credentials a manager took off the record (kept as history); the other values leave them out.",
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "unverified",
                "pending",
                "ended"
              ],
              "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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
                              },
                              "proficiency_text": {
                                "type": "string",
                                "description": "The level's name as the organization calls it (Skills & Credentials › Settings › Proficiency Levels), the platform's default names where it renamed none. Sent on every item of the list, bulk and expiring endpoints as well as show.",
                                "example": "Intermediate"
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2024-01-15"
                              },
                              "expiration_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2025-01-15"
                              },
                              "expiring_soon": {
                                "type": "boolean",
                                "description": "True when the expiration date falls between today and the organization's alert lead time for expiring credentials (Skills & Credentials › Settings › Notifications, \"Credential expiring soon\"). False with no expiration date or once expired.",
                                "example": true
                              },
                              "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"
                              },
                              "pending_upload": {
                                "type": "boolean",
                                "description": "A new certificate the employee uploaded while this credential is still valid, waiting for a manager's review. The credential itself stays verified and live meanwhile, so `verification_status=pending` can return rows with `verified: true`.",
                                "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"
                              },
                              "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"
                                  }
                                }
                              },
                              "detected": {
                                "type": "boolean",
                                "description": "Automatic skill detection added it (`source` is \"inferred\"); the web and phone mark it Detected.",
                                "example": true
                              },
                              "source": {
                                "type": "string",
                                "description": "How the holding was added, as stored (\"inferred\", \"manual\", \"self_reported\", \"external\", …).",
                                "example": "inferred"
                              },
                              "disputed": {
                                "type": "boolean",
                                "description": "Only on the caller's own holdings. They said \"Not mine\" and a reviewer has not decided yet — the holding is then inactive and unverified, which otherwise reads like a rejection.",
                                "example": false
                              },
                              "not_mine_available": {
                                "type": "boolean",
                                "description": "Only on the caller's own holdings. POST /employee_skills/{id}/not_mine would take it off now: detected, active, its detection disputable, Skill Reviews (Competency Assessment) open, and a token that may write skills (`write:skills_certifications`, its own form included).",
                                "example": true
                              }
                            }
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "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\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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
                            },
                            "verification_status": {
                              "type": "string",
                              "description": "`ended`: a manager took it off the record; it is kept as history and held on no date from then on.",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified",
                                "ended"
                              ],
                              "example": "verified"
                            },
                            "current_certification": {
                              "nullable": true,
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/CertificationPeriod"
                                }
                              ]
                            },
                            "certification_history": {
                              "type": "array",
                              "description": "Every award of this credential, newest first: each course, learning path, renewal or upload that awarded it is its own period.",
                              "items": {
                                "$ref": "#/components/schemas/CertificationPeriod"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                            },
                            "verification_status": {
                              "type": "string",
                              "description": "`ended`: a manager took it off the record; it is kept as history and held on no date from then on.",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified",
                                "ended"
                              ],
                              "example": "verified"
                            },
                            "current_certification": {
                              "nullable": true,
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/CertificationPeriod"
                                }
                              ]
                            },
                            "certification_history": {
                              "type": "array",
                              "description": "Every award of this credential, newest first: each course, learning path, renewal or upload that awarded it is its own period.",
                              "items": {
                                "$ref": "#/components/schemas/CertificationPeriod"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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
                            },
                            "verification_status": {
                              "type": "string",
                              "description": "`ended`: a manager took it off the record; it is kept as history and held on no date from then on.",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified",
                                "ended"
                              ],
                              "example": "verified"
                            },
                            "current_certification": {
                              "nullable": true,
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/CertificationPeriod"
                                }
                              ]
                            },
                            "certification_history": {
                              "type": "array",
                              "description": "Every award of this credential, newest first: each course, learning path, renewal or upload that awarded it is its own period.",
                              "items": {
                                "$ref": "#/components/schemas/CertificationPeriod"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "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.\n\nA holding with a validity period on record is ENDED, not deleted: it goes inactive, its\ncurrent validity period ends (`expired`) the day before today, and its history stays on\nrecord. A holding with no validity period is deleted. Only holdings the caller may remove\nthemselves (their own unverified claims and proposals) are accepted; one already ended\nanswers 422 `employee_skill_ended`. Recording that an issuer revoked a credential is a\nmanager's action, not this endpoint's.\n\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill ID",
            "schema": {
              "type": "integer",
              "example": 456
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Employee skill removed (ended with its history kept, or deleted when it had none)"
          },
          "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": "Already ended (`employee_skill_ended`), or the removal was refused (`employee_skill_not_removed`)"
          }
        }
      }
    },
    "/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\nYour own id is always allowed. Anyone else's needs what Team Skills asks\non the web — the Skills & Credentials app admin role, or a manager holding\nthe `skills_certifications` view permission — and the person inside your reach\n(your reports and the people at locations you manage). Requested ids\noutside that reach, or not active members of the business, are omitted\nfrom `users` and listed in `meta.skipped_user_ids` rather than failing\nthe request. A maximum of 100 user ids may be requested at once.\n\n**Token scope:** `read:skills_certifications`. Another person's skills need the wide\nscope itself: a token holding only `read:own_skills_certifications` may fetch the\ncaller's own id and is refused 403 `insufficient_permissions` for anyone else.\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
                                },
                                "proficiency_text": {
                                  "type": "string",
                                  "description": "The level's name as the organization calls it (Skills & Credentials › Settings › Proficiency Levels), the platform's default names where it renamed none. Sent on every item of the list, bulk and expiring endpoints as well as show.",
                                  "example": "Intermediate"
                                },
                                "certification_date": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true,
                                  "example": "2024-01-15"
                                },
                                "expiration_date": {
                                  "type": "string",
                                  "format": "date",
                                  "nullable": true,
                                  "example": "2025-01-15"
                                },
                                "expiring_soon": {
                                  "type": "boolean",
                                  "description": "True when the expiration date falls between today and the organization's alert lead time for expiring credentials (Skills & Credentials › Settings › Notifications, \"Credential expiring soon\"). False with no expiration date or once expired.",
                                  "example": true
                                },
                                "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"
                                },
                                "pending_upload": {
                                  "type": "boolean",
                                  "description": "A new certificate the employee uploaded while this credential is still valid, waiting for a manager's review. The credential itself stays verified and live meanwhile, so `verification_status=pending` can return rows with `verified: true`.",
                                  "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"
                                },
                                "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"
                                    }
                                  }
                                },
                                "detected": {
                                  "type": "boolean",
                                  "description": "Automatic skill detection added it (`source` is \"inferred\"); the web and phone mark it Detected.",
                                  "example": true
                                },
                                "source": {
                                  "type": "string",
                                  "description": "How the holding was added, as stored (\"inferred\", \"manual\", \"self_reported\", \"external\", …).",
                                  "example": "inferred"
                                },
                                "disputed": {
                                  "type": "boolean",
                                  "description": "Only on the caller's own holdings. They said \"Not mine\" and a reviewer has not decided yet — the holding is then inactive and unverified, which otherwise reads like a rejection.",
                                  "example": false
                                },
                                "not_mine_available": {
                                  "type": "boolean",
                                  "description": "Only on the caller's own holdings. POST /employee_skills/{id}/not_mine would take it off now: detected, active, its detection disputable, Skill Reviews (Competency Assessment) open, and a token that may write skills (`write:skills_certifications`, its own form included).",
                                  "example": true
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                              },
                              "proficiency_text": {
                                "type": "string",
                                "description": "The level's name as the organization calls it (Skills & Credentials › Settings › Proficiency Levels), the platform's default names where it renamed none. Sent on every item of the list, bulk and expiring endpoints as well as show.",
                                "example": "Intermediate"
                              },
                              "certification_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2024-01-15"
                              },
                              "expiration_date": {
                                "type": "string",
                                "format": "date",
                                "nullable": true,
                                "example": "2025-01-15"
                              },
                              "expiring_soon": {
                                "type": "boolean",
                                "description": "True when the expiration date falls between today and the organization's alert lead time for expiring credentials (Skills & Credentials › Settings › Notifications, \"Credential expiring soon\"). False with no expiration date or once expired.",
                                "example": true
                              },
                              "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"
                              },
                              "pending_upload": {
                                "type": "boolean",
                                "description": "A new certificate the employee uploaded while this credential is still valid, waiting for a manager's review. The credential itself stays verified and live meanwhile, so `verification_status=pending` can return rows with `verified: true`.",
                                "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"
                              },
                              "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"
                                  }
                                }
                              },
                              "detected": {
                                "type": "boolean",
                                "description": "Automatic skill detection added it (`source` is \"inferred\"); the web and phone mark it Detected.",
                                "example": true
                              },
                              "source": {
                                "type": "string",
                                "description": "How the holding was added, as stored (\"inferred\", \"manual\", \"self_reported\", \"external\", …).",
                                "example": "inferred"
                              },
                              "disputed": {
                                "type": "boolean",
                                "description": "Only on the caller's own holdings. They said \"Not mine\" and a reviewer has not decided yet — the holding is then inactive and unverified, which otherwise reads like a rejection.",
                                "example": false
                              },
                              "not_mine_available": {
                                "type": "boolean",
                                "description": "Only on the caller's own holdings. POST /employee_skills/{id}/not_mine would take it off now: detected, active, its detection disputable, Skill Reviews (Competency Assessment) open, and a token that may write skills (`write:skills_certifications`, its own form included).",
                                "example": true
                              }
                            }
                          },
                          {
                            "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
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/categories": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get employee skills by category",
        "description": "Get employee's skills grouped by category with statistics\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/{id}/request_verification": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Request skill verification",
        "description": "Request verification for an employee skill from manager\n\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                        },
                        "proficiency_text": {
                          "type": "string",
                          "description": "The level's name as the organization calls it (Skills & Credentials › Settings › Proficiency Levels), the platform's default names where it renamed none. Sent on every item of the list, bulk and expiring endpoints as well as show.",
                          "example": "Intermediate"
                        },
                        "certification_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2024-01-15"
                        },
                        "expiration_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "example": "2025-01-15"
                        },
                        "expiring_soon": {
                          "type": "boolean",
                          "description": "True when the expiration date falls between today and the organization's alert lead time for expiring credentials (Skills & Credentials › Settings › Notifications, \"Credential expiring soon\"). False with no expiration date or once expired.",
                          "example": true
                        },
                        "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"
                        },
                        "pending_upload": {
                          "type": "boolean",
                          "description": "A new certificate the employee uploaded while this credential is still valid, waiting for a manager's review. The credential itself stays verified and live meanwhile, so `verification_status=pending` can return rows with `verified: true`.",
                          "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"
                        },
                        "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"
                            }
                          }
                        },
                        "detected": {
                          "type": "boolean",
                          "description": "Automatic skill detection added it (`source` is \"inferred\"); the web and phone mark it Detected.",
                          "example": true
                        },
                        "source": {
                          "type": "string",
                          "description": "How the holding was added, as stored (\"inferred\", \"manual\", \"self_reported\", \"external\", …).",
                          "example": "inferred"
                        },
                        "disputed": {
                          "type": "boolean",
                          "description": "Only on the caller's own holdings. They said \"Not mine\" and a reviewer has not decided yet — the holding is then inactive and unverified, which otherwise reads like a rejection.",
                          "example": false
                        },
                        "not_mine_available": {
                          "type": "boolean",
                          "description": "Only on the caller's own holdings. POST /employee_skills/{id}/not_mine would take it off now: detected, active, its detection disputable, Skill Reviews (Competency Assessment) open, and a token that may write skills (`write:skills_certifications`, its own form included).",
                          "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"
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrors"
                }
              }
            }
          }
        }
      }
    },
    "/employee_skills/{id}/not_mine": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Not mine — take a detected skill off your profile",
        "description": "The phone My Skills card's \"Not mine\" on a skill automatic skill detection added\n(`detected: true`, `not_mine_available: true`). The holding becomes inactive and\nunverified, its detection is marked disputed, and a reviewer is asked to remove it or\nput it back. Only the caller's own holding (another person's id is 404). Refused (422)\nwith `detection_paused` while Competency Assessment is off, `not_detected` for a skill\ndetection did not add (or already disputed), `already_disputed` / `already_decided`\nwhen the detection changed meanwhile.\n\n**Token scope:** `write:skills_certifications` (its `own_` form also satisfies it).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Employee skill (holding) ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Taken off — the holding, detailed",
            "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
                            },
                            "verification_status": {
                              "type": "string",
                              "description": "`ended`: a manager took it off the record; it is kept as history and held on no date from then on.",
                              "enum": [
                                "verified",
                                "pending",
                                "unverified",
                                "ended"
                              ],
                              "example": "verified"
                            },
                            "current_certification": {
                              "nullable": true,
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/CertificationPeriod"
                                }
                              ]
                            },
                            "certification_history": {
                              "type": "array",
                              "description": "Every award of this credential, newest first: each course, learning path, renewal or upload that awarded it is its own period.",
                              "items": {
                                "$ref": "#/components/schemas/CertificationPeriod"
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not the caller's holding (employee_skill_not_found)"
          },
          "422": {
            "description": "Refused — error.code detection_paused | not_detected | already_disputed | already_decided, with the reason"
          }
        }
      }
    },
    "/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\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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. `ended` lists credentials a manager took off the record; the other values leave those out.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "expired",
                "expiring",
                "ended"
              ],
              "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "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": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "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\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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",
                              "description": "`ended`: a manager took it off the record; kept as history.",
                              "enum": [
                                "active",
                                "expiring_soon",
                                "expiring_notice",
                                "expired",
                                "ended"
                              ],
                              "example": "active"
                            },
                            "document_attached": {
                              "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"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/expiring": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get expiring certifications",
        "description": "Get certifications that are expiring soon with urgency levels\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/expired": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get expired certifications",
        "description": "Get certifications that have already expired\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/certifications/summary": {
      "get": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Get certification summary",
        "description": "Get summary statistics of employee's certifications\n\n**Token scope:** `read:skills_certifications`; `read:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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",
                          "description": "Certifications expiring within the organization's expiry-alert lead time (Settings › Notifications; 30 days unless changed).",
                          "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",
                                "description": "As the top-level expiring_soon, for this category."
                              }
                            }
                          }
                        },
                        "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"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\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",
                              "description": "`ended`: a manager took it off the record; kept as history.",
                              "enum": [
                                "active",
                                "expiring_soon",
                                "expiring_notice",
                                "expired",
                                "ended"
                              ],
                              "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"
                }
              }
            }
          },
          "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/upload_document": {
      "post": {
        "tags": [
          "Skills & Certifications"
        ],
        "summary": "Upload certification document",
        "description": "Upload a document file for a certification\n\n**Token scope:** `write:skills_certifications`; `write:own_skills_certifications`, what a\ntoken made by hand carries, also satisfies it and reaches the caller's own records.\n",
        "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      }
    },
    "/safety_hub/dashboard": {
      "get": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Dashboard KPI tiles",
        "description": "Returns the caller's home-screen KPI tiles in display order. Each tile\nis self-describing — it carries its own label, count, and Bootstrap\ntext color class so native-mobile clients render the same palette as\nthe web Safety Hub dashboard. The list is keyed and ordered, so a\nclient that iterates it renders either view correctly.\n\nWhich tiles depends on who is asking:\n\n  * **Safety Hub managers** (manager_or_above, or a Safety Hub app\n    admin) whose token carries `read:safety_hub` get **five TEAM\n    tiles**: the team's numbers, with incidents and expiring\n    certifications limited to the sites they can see.\n  * **Everyone else** — members, and a manager whose token carries only\n    `read:own_safety_hub` — gets **four PERSONAL tiles**, labelled\n    \"My …\": their own certifications, the observations they recorded,\n    the incidents they reported, and the talks they facilitate or are\n    expected at. `days_without_injury` has no personal form and is\n    omitted rather than reported misleadingly.\n\nTiles, in order (team labels shown; personal labels prefix \"My \"):\n  1. `days_without_injury` (team only) — whole days since the most\n     recent injury-type incident. `count` is **null** when the business\n     has no injury on record — the best state, not zero days — and the\n     label then reads \"No Injuries on Record\" instead of \"Days Without\n     Injury\". Color: `text-success`.\n  2. `certifications_expiring_soon` — certifications expiring within\n     the business-configured reminder window (default 30 days),\n     aggregated across EmployeeSkill and LmsTrainingRecord. Color:\n     `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 — five team tiles for a Safety Hub manager whose token carries read:safety_hub, otherwise four personal tiles (no days_without_injury).",
                      "minItems": 4,
                      "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. Personal tiles are prefixed \"My \"; days_without_injury reads \"No Injuries on Record\" when its count is null.",
                            "example": "Days Without Injury"
                          },
                          "count": {
                            "type": "integer",
                            "nullable": true,
                            "minimum": 0,
                            "description": "Numeric value for the tile. Null only for days_without_injury when no injury is on record (render the label, not a number).",
                            "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",
        "description": "The reporter, the assigned investigator and a manager of the incident's\nsite get `incident` + `permissions` (a reporter who is the injured person\nalso gets their own row as `your_injury`). Anyone else named as an\ninjured person on the incident gets ONLY `own_injury_record` — when,\nwhere and what happened, who is handling it, and their own injury row;\nnever the reporter, witnesses, other people or the investigation. Any\nother caller gets 403.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Incident detail, or the caller's own injury record",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    },
                    "your_injury": {
                      "nullable": true,
                      "type": "object",
                      "properties": {
                        "injury": {
                          "type": "string"
                        },
                        "body_parts_affected": {
                          "type": "string",
                          "nullable": true
                        },
                        "medical_attention": {
                          "type": "boolean"
                        },
                        "hospitalized": {
                          "type": "boolean"
                        },
                        "days_away_from_work": {
                          "type": "integer"
                        },
                        "days_restricted_work": {
                          "type": "integer"
                        },
                        "return_to_work_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "return_to_full_duty": {
                          "type": "boolean",
                          "nullable": true
                        },
                        "work_restrictions": {
                          "type": "string",
                          "nullable": true
                        },
                        "osha_case_number": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "own_injury_record": {
                      "type": "object",
                      "description": "Returned INSTEAD of incident/permissions to a caller who may see only their own injury record.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "reference": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "occurred_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "location": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "investigator": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "url": {
                          "type": "string"
                        },
                        "your_injury": {
                          "type": "object",
                          "properties": {
                            "injury": {
                              "type": "string"
                            },
                            "body_parts_affected": {
                              "type": "string",
                              "nullable": true
                            },
                            "medical_attention": {
                              "type": "boolean"
                            },
                            "hospitalized": {
                              "type": "boolean"
                            },
                            "days_away_from_work": {
                              "type": "integer"
                            },
                            "days_restricted_work": {
                              "type": "integer"
                            },
                            "return_to_work_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true
                            },
                            "return_to_full_duty": {
                              "type": "boolean",
                              "nullable": true
                            },
                            "work_restrictions": {
                              "type": "string",
                              "nullable": true
                            },
                            "osha_case_number": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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),\n**`anonymous` is fixed once the incident is reported** (a different value\nis a 422 — the desktop edit form no longer offers it either),\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",
                        "description": "Fixed when the incident is reported. Omit it, or send the\nincident's current value; a different value is refused with\n422 and nothing is saved.\n"
                      },
                      "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"
                      }
                    }
                  },
                  "link_people": {
                    "type": "object",
                    "description": "\"Their account\" for people already on the incident: a map of\nperson row id to the account id in this business (\"\" clears\nit). A newly linked injured person is told and can open their\nown injury record; an id that is no account here comes back\nin `warnings`.\n",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "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 (including an attempt to change `anonymous`), 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/incidents/{id}/cancel": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel a duplicate or mistaken incident report",
        "description": "The API twin of the desktop and phone Cancel. The reporter may cancel\ntheir own report while it is still `reported`; a site manager may\ncancel one within their sites (`Incident#cancellable_by?`). A closed,\nOSHA-recordable or WCB-reportable case, or one with open corrective\nactions, is refused with the reason. The reason goes on the incident's\ntimeline, an open investigation is closed with it, and the report leaves\nevery incident total. A cancel is a final act, so the token needs\n**`destructive:safety_hub`**; the detail read's `permissions.can_cancel`\nsays whether this door is open to the caller.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cancellation_reason"
                ],
                "properties": {
                  "cancellation_reason": {
                    "type": "string",
                    "description": "Why the report is cancelled. Required and non-blank."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/IncidentDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubIncidentPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not its reporter (while Reported) or a site manager, the case cannot be cancelled, or the token lacks destructive:safety_hub"
          },
          "404": {
            "description": "Incident not found"
          },
          "422": {
            "description": "Unprocessable — the reason is blank"
          }
        }
      }
    },
    "/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"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyObservationPermissions"
                    }
                  }
                }
              }
            }
          },
          "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). A manager setting `status`\nto `closed` closes the observation — a final act (owner decision\n2026-10-04, #6721): that request also requires `destructive:safety_hub`,\non top of the write scope. **`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": {
                      "$ref": "#/components/schemas/SafetyObservationPermissions"
                    },
                    "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": "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). A manager setting `status`\nto `closed` closes the observation — a final act (owner decision\n2026-10-04, #6721): that request also requires `destructive:safety_hub`,\non top of the write scope. **`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": {
                      "$ref": "#/components/schemas/SafetyObservationPermissions"
                    },
                    "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)"
          }
        }
      }
    },
    "/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": {
                      "$ref": "#/components/schemas/SafetyObservationPermissions"
                    }
                  }
                }
              }
            }
          },
          "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."
                    },
                    "own_injury_records": {
                      "type": "array",
                      "description": "Personal feed only — incidents someone else reported that name the caller as an injured person (up to 10, newest first). Empty for team=true.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "reference": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "occurred_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "location": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "url": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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}/start": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Start work on a corrective action",
        "description": "Start work on ONE corrective/preventive action (Capa::Action): Not\nstarted (`pending`) → In progress, through `Capa::Action#start!`, which\nrecords who started it on the action's trail. This is the only way an\naction comes to read In progress — reassigning it never moves the status.\nThe desktop action page's Start work, for the native app.\n\nAuthority is the complete endpoint's and the detail endpoint's\n`permissions.can_start`: a **safety-hub manager** OR the action's\n**assignee**, resolved through the same desktop `#visible_scope` (an\naction the caller may not reach is a 404). Only a Not started action can\nbe started; anything else is 422.\n\nRequires the **write** scope: `write:safety_hub`, or `write:own_safety_hub`\nfor your OWN assigned action. Gated on the tenant's `incidents_enabled`\nmodule toggle.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Work started — the action is In progress",
            "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, a token without the write scope, or Safety Hub / the Incidents module disabled"
          },
          "404": {
            "description": "Corrective action not found (or not visible to the caller)"
          },
          "422": {
            "description": "Unprocessable — work has already started, or the action is completed/cancelled"
          }
        }
      }
    },
    "/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!` (keeps the status — reassigning is\nnot starting — 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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — you may only view permits you requested, authorised or are on the crew of"
          },
          "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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "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": "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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "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)"
          }
        }
      }
    },
    "/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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "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}/approve": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Issue (approve) a permit",
        "description": "Issue ONE requested permit — the mockup's permit-detail \"Approve / issue\"\ncontrol, and the native twin of the desktop\n`Apps::SafetyHub::PermitsController#approve`. Moves a `requested` permit →\n`approved`, stamping the **authoriser** (`approved_by`), the timestamp and\nany `approval_notes`, and (for a contractor permit) the current\npre-qualification. The permit becomes live inside its work window.\n\n**Manager-only within accessible sites** — the desktop wires approve behind\n`authorize_safety_hub_manager!` + `authorize_permit_authoriser!`\n(authorising a permit is a manager act at the permit's site), the SAME\nchain suspend and resume use. A non-manager is refused, as is a site-scoped\nmanager whose accessible sites exclude the permit. The wide\n`write:safety_hub` scope is required **unconditionally** — issuing is\nalways a management action, so there is no own-scoped case. Gated on the\ntenant's `permits_enabled` module toggle.\n\n**Two-person control:** a permit is a two-person control, so the model\nrefuses a requester authorising their OWN permit (`by == requested_by`).\nIssuing also requires **every precaution confirmed**, the work window not\nyet ended, and — for a contractor permit, when the tenant requires it — a\ncurrent pre-qualification; any of these is a **422**\n(`WorkPermit#approvable_reasons`), and only a `requested` permit can be\nissued at all (a `draft`/`approved`/`suspended`/`closed`/`cancelled`/\n`expired` permit is likewise a 422).\n\nThe authoriser may confirm the **last outstanding precautions in the same\nPOST** by sending a `work_permit` payload (hazards / precaution checklist /\ncontrol notes) — saved first, then the permit is issued. On success the\nrequester is notified their permit was issued\n(`notify_permit_approved`). The response re-reads the full permit detail\n(same eager-load set as the detail read — no N+1) with the refreshed viewer\npermissions.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "approval_notes": {
                    "type": "string",
                    "description": "Optional free-text notes recorded on `approval_notes`."
                  },
                  "proceed_anyway": {
                    "type": "boolean",
                    "description": "Required (true) when the contractor has certificate gaps (`certificate_gaps` on the permit detail) — the explicit confirmation to issue regardless. Ignored when there are none."
                  },
                  "certificate_override_reason": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Required, non-blank, with `proceed_anyway` when the contractor has certificate gaps. Stored on the permit with the gaps confirmed, who and when (`certificate_override`)."
                  },
                  "confirmed_gap_keys": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Required when the contractor has certificate gaps: the `key` of every gap you showed the user (from `certificate_gaps` on the permit detail). A live gap missing from this list is refused (`certificate_gaps_changed: true`) rather than recorded as confirmed unseen; a listed gap that has since cleared is simply not recorded."
                  },
                  "work_permit": {
                    "type": "object",
                    "description": "Optional — confirm the last precautions (and any working fields) in the same request before issuing. Only a manager reaches this action, so these are always the authoriser's to tick.",
                    "properties": {
                      "hazards": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "precautions": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "boolean"
                        },
                        "description": "Map of the permit type's precaution text → confirmed (true). Only keys in the type's catalog are recorded."
                      },
                      "isolations": {
                        "type": "string"
                      },
                      "ppe": {
                        "type": "string"
                      },
                      "gas_test": {
                        "type": "string"
                      },
                      "emergency_arrangements": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit issued (permit is now `approved`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the permit is at a site the manager does not manage, an own-scoped token (wide scope required), or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit is not awaiting approval, the requester cannot authorise their own permit, precautions are outstanding, the window has ended, the contractor is not pre-qualified, or the contractor has certificate gaps and `proceed_anyway` + `certificate_override_reason` + `confirmed_gap_keys` did not confirm them all (then the body carries `confirmation_required: true`, `certificate_gaps_changed` and the current `certificate_gaps`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SafetyHubPermitCertificateGapRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:safety_hub` scope.\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": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "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/permits/{id}/close": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Close a permit",
        "description": "Close out ONE issued permit — the mockup's permit-detail \"Close permit\"\ncontrol, and the native twin of the desktop\n`Apps::SafetyHub::PermitsController#close`. This is the clean **sign-off**\nof a completed job (distinct from cancel, the \"scrap it\" door): the caller\nconfirms the area was left safe and the permit moves\n`approved`/`suspended`/`expired` → `closed`, recording the closer, the\ntimestamp and any closure notes. An `expired` permit is one the hourly sweep\nretired a day after its end time because nobody closed it out; its close-out\nis still owed, so it stays closable (`permissions.can_close`).\n\n**Authority** is the desktop close gate exactly — which admits a persona\nthe edit door does not, the **authoriser**: the **requester**, the\n**authoriser** (`approved_by`), or a **safety_hub_manager** at the permit's\nsite may close. A\nparticipant (requester or authoriser) closing their OWN permit needs only\nthe narrow `write:own_safety_hub` scope; a manager closing a permit they\nare not personally on acts via manager privilege and needs the wide\n`write:safety_hub`. Gated on the tenant's `permits_enabled` module toggle.\n\n`area_left_safe` must be confirmed **true** — a false/absent value is\nrefused with **422** (`WorkPermit#close!` requires it), never a silent\nclose. Only an **approved**, **suspended** or **expired** permit can be\nclosed; a `draft`/`requested`/`closed`/`cancelled` permit is a **422**.\nClose fires **no notification**. The response re-reads the full permit\ndetail (same eager-load set as the detail read — no N+1) with the refreshed\nviewer permissions, so the client repaints the detail — the `closed` state\nand closure record — without a second round trip.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:safety_hub` scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "area_left_safe"
                ],
                "properties": {
                  "area_left_safe": {
                    "type": "boolean",
                    "description": "Must be true — confirms the work area was left safe. A false/absent value is refused with 422."
                  },
                  "closure_notes": {
                    "type": "string",
                    "description": "Optional free-text closure notes recorded on the permit."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit closed (permit is now `closed`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not the requester, the authoriser 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 — area_left_safe was not confirmed, or the permit is not approved/suspended/expired (only an issued, suspended or expired permit can be closed)"
          }
        }
      }
    },
    "/safety_hub/permits/{id}/suspend": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Suspend a permit",
        "description": "Suspend ONE issued permit — the mockup's permit-detail \"Suspend\" control,\nand the native twin of the desktop\n`Apps::SafetyHub::PermitsController#suspend`. Flips an issued (`approved`)\npermit → `suspended` (work must stop) and records the reason; the permit\nstays **open** and can later be resumed or closed.\n\n**Manager-only within accessible sites** — the desktop wires suspend behind\n`authorize_safety_hub_manager!` + `authorize_permit_authoriser!`\n(authorising a permit is a manager act at the permit's site). A non-manager\nis refused, as is a site-scoped manager whose accessible sites exclude the\npermit. The wide `write:safety_hub` scope is required **unconditionally** —\na suspension is always a management action, so there is no own-scoped case.\nGated on the tenant's `permits_enabled` module toggle. Offered to the\nclient as `permissions.can_suspend`, which is false for any caller or\ntoken this endpoint would refuse.\n\nOnly an **issued (approved)** permit can be suspended; a\n`draft`/`requested`/`suspended`/`closed`/`cancelled`/`expired` permit is\nrefused with **422**. On success the permit holders are notified (work must\nstop, except the actor). The response re-reads the full permit detail (same\neager-load set as the detail read — no N+1) with the refreshed viewer\npermissions, so the client repaints the detail — the `suspended` state and\n`suspended_reason` — without a second round trip.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:safety_hub` scope.\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 `suspended_reason`. Defaults to \"Suspended by {name}\" when blank."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit suspended (permit is now `suspended`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the permit is at a site the manager does not manage, an own-scoped token (wide scope required), or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit is not issued (only an issued/approved permit can be suspended)"
          }
        }
      }
    },
    "/safety_hub/permits/{id}/resume": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resume a suspended permit",
        "description": "Resume ONE suspended permit — the mockup's permit-detail \"Resume\" control,\nand the native twin of the desktop\n`Apps::SafetyHub::PermitsController#resume`. The **inverse of suspend**:\nflips a `suspended` permit back → `approved` (work may restart) and clears\n`suspended_reason`; the permit stays **open**.\n\n**Manager-only within accessible sites** — the desktop wires resume behind\n`authorize_safety_hub_manager!` + `authorize_permit_authoriser!`\n(authorising a permit is a manager act at the permit's site), the SAME\nchain approve and suspend use. A non-manager is refused, as is a\nsite-scoped manager whose accessible sites exclude the permit. The wide\n`write:safety_hub` scope is required **unconditionally** — resuming is\nalways a management action, so there is no own-scoped case. Gated on the\ntenant's `permits_enabled` module toggle. Offered to the client as\n`permissions.can_resume`, which is false for any caller or token this\nendpoint would refuse (a certificate gap leaves it true — see below).\n\nOnly a **suspended** permit can be resumed; a\n`draft`/`requested`/`approved`/`closed`/`cancelled`/`expired` permit is\nrefused with **422**. Resume fires **no notification** (matching the web).\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 — the restored `approved` state and the now-cleared\n`suspended_reason` — without a second round trip.\n\n**Contractor certificate gaps (CTR-030).** Resuming puts a crew back on\nsite, so while the contractor has `certificate_gaps` the call needs the\nsame `proceed_anyway` + `certificate_override_reason` +\n`confirmed_gap_keys` as issuing. The confirmation is recorded against the\nRESUMER (`certificate_override.action: resumed`), even when the issuer\nhad overridden the same gaps.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SafetyHubPermitCertificateConfirmation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permit resumed (permit is now `approved`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the permit is at a site the manager does not manage, an own-scoped token (wide scope required), or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit is not suspended (only a suspended permit can be resumed), or the contractor has certificate gaps that were not all confirmed (then the body carries `confirmation_required: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SafetyHubPermitCertificateGapRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/safety_hub/permits/{id}/confirm_certificate_cover": {
      "post": {
        "tags": [
          "Safety Hub"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record that a contractor's cover was confirmed",
        "description": "\"Cover confirmed\" (CTR-030) — the answer to the day-before\ncontractor-certificate ask on an ISSUED permit, and the native twin of\nthe desktop `Apps::SafetyHub::PermitsController#confirm_certificate_cover`.\nThe authoriser checked with the contractor that cover is in place (for\nexample a renewal emailed but not yet on file); the reason, the gaps\nconfirmed, who and when are recorded on the permit\n(`certificate_override.action: cover_confirmed`, earlier confirmations\nkept) and the open day-before asks close.\n\n**Manager-only within accessible sites** — the same authoriser chain as\napprove / suspend / resume, and the wide `write:safety_hub` scope\nunconditionally. Offered to the client as\n`permissions.can_confirm_certificate_cover`. A permit that is not issued,\nor whose contractor has no certificate gaps, is a **422**.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "certificate_override_reason",
                  "confirmed_gap_keys"
                ],
                "properties": {
                  "certificate_override_reason": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "How cover was confirmed. Required, non-blank."
                  },
                  "confirmed_gap_keys": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The `key` of every gap you showed the user. A live gap missing from this list is refused (`certificate_gaps_changed: true`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cover confirmed and recorded on the permit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permit": {
                      "$ref": "#/components/schemas/SafetyHubPermitDetail"
                    },
                    "permissions": {
                      "$ref": "#/components/schemas/SafetyHubPermitPermissions"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — not a manager, the permit is at a site the manager does not manage, an own-scoped token (wide scope required), or Safety Hub / the Permits module disabled"
          },
          "404": {
            "description": "Permit not found"
          },
          "422": {
            "description": "Unprocessable — the permit is not issued, the contractor has no certificate gaps, or the reason / `confirmed_gap_keys` did not confirm every live gap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SafetyHubPermitCertificateGapRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/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\nEach Q&A pair appears ONCE: for an FAQ with authored pairs, the first\npair is `question`/`answer` and `faq_items` holds only the pairs after\nit — render `question`/`answer`, then `faq_items`. An FAQ with no\nauthored pairs returns `faq_items: []`.\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 (the start date)\n  * `week_1`      — day_offset 1..7\n  * `month_1`     — day_offset 8..30\n  * `month_2_3`   — day_offset 31..90\n  * `beyond`      — day_offset >= 91\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` is the new hire's own figure\n(`RecruitingOnboardingPlan#new_hire_progress` — the one the portal\nheader, its My Progress tab and the phone show): every required item\non the plan, and its required welcome items while the welcome packet\nis on, with partial credit (0.75) for documents in the `uploaded`\nstate. Rounded to the nearest whole number, never 0 once anything\ncounts and never 100 until everything is done, so it can diverge\nslightly from a naïve completed/total ratio over all phases.\n\n`progress_shown` is the tenant's \"Progress Tracking\" setting. When it\nis `false` the hire is shown no percentage and no days left on any\nsurface, so `overall_progress`, `days_remaining` and each phase's\n`progress` are `null`; the item counts and items are still sent.\n\n**Required Scopes:** `read:onboarding` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; the scope Ask AI and MCP ask for the\nOnboarding agent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:onboarding\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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",
                        "progress_shown",
                        "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"
                        },
                        "progress_shown": {
                          "type": "boolean",
                          "description": "Whether the hire is shown their progress (the\n\"Progress Tracking\" setting). `false` nulls\n`overall_progress`, `days_remaining` and each phase's\n`progress`.\n",
                          "example": true
                        },
                        "days_remaining": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null when `progress_shown` is false.",
                          "example": 39
                        },
                        "overall_progress": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Plan-wide completion percent (0-100), the figure\nthe hire's portal shows\n(RecruitingOnboardingPlan#new_hire_progress),\nincluding partial credit for uploaded-but-unverified\ndocuments; 100 only once every required item is done.\nNull when `progress_shown` is false.\n",
                          "example": 29
                        }
                      }
                    },
                    "phases": {
                      "type": "array",
                      "description": "Always six entries, in fixed order. Empty phases\nare returned with `items: []` and `progress: 0` (null\nwhen `progress_shown` is false) so the client can\nrender 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",
                            "nullable": true,
                            "description": "Per-phase completion percent (0-100). Null when `progress_shown` is false.",
                            "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."
          },
          "403": {
            "description": "The token lacks `read:onboarding` (`insufficient_permissions`), or Onboarding Hub is not enabled for the business (`app_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"
                  ]
                }
              }
            }
          },
          "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 active plans on the caller's\ndesktop plans list (their purview; a people leader on the employee\nrole also gets their team's plans) — status one of `active`,\n`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\n**Required Scopes:** `read:onboarding` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; the scope Ask AI and MCP ask for the\nOnboarding agent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:onboarding\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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."
          },
          "403": {
            "description": "The token lacks `read:onboarding` (`insufficient_permissions`), Onboarding Hub is not enabled for the business (`app_not_enabled`), or the caller may not open the team plans list (`forbidden`): not an administrator, manager, Onboarding Hub administrator, HR coordinator or people leader.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/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\nA people leader on the employee role (reports here, no manager role)\nis admitted, as on the desktop plans list, and sees their team's\nitems. Rows from plans they may see but not open carry\n`openable: false` with `web_url` and `mobile_url` null — the client\ndraws them without a link, as the desktop list does.\n\n**Required Scopes:** `read:onboarding` (#7183, owner rule 2026-10-06\n\"every api/v1 door asks a scope\"; the scope Ask AI and MCP ask for the\nOnboarding agent). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:onboarding\") before anything else is checked. The\nsign-in tokens hold it; a browser session is asked no scope.\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",
                          "openable",
                          "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
                          },
                          "openable": {
                            "type": "boolean",
                            "description": "`false` for a row the caller may see but not open\n(a people leader's team plans); `web_url` and\n`mobile_url` are then null. `true` for every other\ncaller's rows.\n",
                            "example": true
                          },
                          "web_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "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",
                            "nullable": true,
                            "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."
          },
          "403": {
            "description": "The token lacks `read:onboarding` (`insufficient_permissions`), Onboarding Hub is not enabled for the business (`app_not_enabled`), or the caller may not open the team plans list (`forbidden`): not an administrator, manager, Onboarding Hub administrator, HR coordinator or people leader.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/plans/{id}": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read one onboarding plan (admin)",
        "description": "The state of one onboarding plan, for a system that follows the\n`onboarding_plan.completed` and `onboarding_plan.reopened` webhook\nevents (their `read_path` is this\nendpoint). State, ids and item counts by status — no answers.\n\n**Who:** the people who manage every plan: account administrators,\nOnboarding Hub administrators and HR coordinators. Anyone else gets 403.\n\n**Required Scopes:** `read:onboarding`\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Onboarding plan ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The plan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan"
                  ],
                  "properties": {
                    "plan": {
                      "type": "object",
                      "required": [
                        "id",
                        "status",
                        "items"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "user_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The new hire. Null for a pre-hire with no account yet."
                        },
                        "status": {
                          "type": "string"
                        },
                        "plan_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "plan_type": {
                          "type": "string",
                          "nullable": true
                        },
                        "job_title": {
                          "type": "string",
                          "nullable": true
                        },
                        "start_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "completion_percentage": {
                          "type": "number"
                        },
                        "hiring_manager_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "items": {
                          "type": "object",
                          "description": "For tasks, forms and documents, how many items are in each status.",
                          "properties": {
                            "tasks": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "integer"
                              }
                            },
                            "forms": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "integer"
                              }
                            },
                            "documents": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `read:onboarding`, Onboarding Hub is not enabled, or the caller does not manage every plan"
          },
          "404": {
            "description": "No such plan in this business"
          }
        }
      }
    },
    "/onboarding/items/{id}": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read one onboarding form item (admin)",
        "description": "The state of one form item on an onboarding plan, for a system that\nfollows the `onboarding_item.verified` webhook event (its `read_path` is\nthis endpoint). State and ids only — never an answer. The profile changes\nthe submission filed are listed in `change_request_ids`; read each at\n`GET /api/v1/people/changes/{id}` (`read:people_changes`).\n\n**Who:** the people who manage every plan: account administrators,\nOnboarding Hub administrators and HR coordinators. Anyone else gets 403.\n\n**Required Scopes:** `read:onboarding`\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Onboarding form item ID (`enrollment_form_id` in the event)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item"
                  ],
                  "properties": {
                    "item": {
                      "type": "object",
                      "required": [
                        "id",
                        "plan_id",
                        "status",
                        "requires_hr_verification",
                        "change_request_ids"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "plan_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "null for an item no onboarding plan holds: a library form, or an I-9 Section 3 reverification held on the employee (`user_id` names them)."
                        },
                        "user_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The person the item is about: the plan's employee, or the employee a Section 3 held on them reverifies."
                        },
                        "name": {
                          "type": "string"
                        },
                        "form_type": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "in_progress",
                            "submitted",
                            "completed",
                            "skipped"
                          ],
                          "description": "`submitted` means sent by the hire and waiting for HR to verify."
                        },
                        "due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "requires_hr_verification": {
                          "type": "boolean"
                        },
                        "submitted_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "verified_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "verified_by_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "form_submission_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "The Forms submission behind a Forms-backed item."
                        },
                        "change_request_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `read:onboarding`, Onboarding Hub is not enabled, or the caller does not manage every plan"
          },
          "404": {
            "description": "No such item in this business"
          }
        }
      }
    },
    "/offboarding/plans": {
      "get": {
        "tags": [
          "Offboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List offboarding plans",
        "description": "The offboarding plans the caller may see, newest first (`created_at`\ndescending). Reach is the desktop's (`Offboarding::PlanAccess`): an\nadmin, an Offboarding app admin or HR sees every plan in the\norganization; a manager sees only the plans of their own reports. Nobody\nsees a plan about themselves while it is still a draft.\n\nEach row is the plan's summary shape — the plan's own\n`GET /offboarding/plans/{id}` adds the termination details, the\nsuccessor, the creator and the task and document counts.\n\nFilters, all optional and combined with AND:\n- `status` — one or more of `draft`, `active`, `completed`, `cancelled`,\n  comma-separated (`?status=draft,active` returns either).\n- `plan_type` — one of `voluntary`, `involuntary`, `retirement`,\n  `end_of_contract`, `layoff`.\n- `user_id` / `manager_id` — one id, or a list of ids\n  (`?user_id[]=1&user_id[]=2`). They only narrow what the caller may\n  already see.\n\n**An unusable filter is refused, never ignored:** an unknown `status`\nor `plan_type` value, or an id filter sent as an object\n(`?user_id[a]=1`), answers `400 invalid_filter` naming the accepted\nvalues.\n\n**Required Scopes:** `read:offboarding` (or `read:own_offboarding`, or\n`admin`). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:offboarding\"), checked after the app and\nmanage-offboarding gates. A browser or WebView session is asked no\nscope.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Comma-separated plan statuses. Any value outside `draft`, `active`, `completed`, `cancelled` is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "example": "draft,active"
            }
          },
          {
            "name": "plan_type",
            "in": "query",
            "required": false,
            "description": "Any other value is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "enum": [
                "voluntary",
                "involuntary",
                "retirement",
                "end_of_contract",
                "layoff"
              ]
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "The departing employee. One id, or a list (`user_id[]=1&user_id[]=2`); an object-shaped value is a 400 `invalid_filter`.",
            "schema": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                }
              ]
            }
          },
          {
            "name": "manager_id",
            "in": "query",
            "required": false,
            "description": "The plan's manager. One id, or a list (`manager_id[]=1&manager_id[]=2`); an object-shaped value is a 400 `invalid_filter`.",
            "schema": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                }
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Missing or non-positive means 25; above 100 is clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the plans the caller may see, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OffboardingPlanSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "`invalid_filter` — an unknown `status` or `plan_type` value, or a\n`user_id` / `manager_id` sent as an object. The message names the\naccepted values.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `offboarding_not_enabled` — the Offboarding app is not enabled for\n  this organization.\n- `insufficient_permissions` — the app is on but not open to the caller\n  (\"You do not have access to Offboarding\"), the caller may not manage\n  offboarding (\"You do not have access to offboarding management features\"),\n  or the token lacks the scope (\"Required scope: read:offboarding\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/offboarding/tasks": {
      "get": {
        "tags": [
          "Offboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List one offboarding plan's tasks",
        "description": "The checklist tasks of one offboarding plan, named by the required\n`plan_id`, in the plan's sequence (`sequence_order`, then creation\norder, so paging never repeats or skips a task).\n\nThe caller must be able to see that plan — the same reach as\n`GET /offboarding/plans` (an admin, an Offboarding app admin or HR for\nany plan; a manager for their own reports' plans) — or the list is\nrefused with `403 unauthorized`. Each row is the task's summary shape;\n`GET /offboarding/tasks/{id}` adds the description, timing and\ncompletion details.\n\nFilters, optional and combined with AND:\n- `status` — one or more of `pending`, `in_progress`, `completed`,\n  `skipped`, `blocked`, comma-separated.\n- `department` — one of `IT`, `HR`, `Facilities`, `Payroll`, `Manager`,\n  `Legal`, `Finance`, `Operations` (case-sensitive).\n\nAn unknown `status` or `department` value answers `400 invalid_filter`\nnaming the accepted values, never an empty list.\n\n**Required Scopes:** `read:offboarding` (or `read:own_offboarding`, or\n`admin`). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:offboarding\"), checked after the app and\nmanage-offboarding gates. A browser or WebView session is asked no\nscope.\n",
        "parameters": [
          {
            "name": "plan_id",
            "in": "query",
            "required": true,
            "description": "The offboarding plan whose tasks to list.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Comma-separated task statuses. Any value outside `pending`, `in_progress`, `completed`, `skipped`, `blocked` is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "example": "pending,in_progress"
            }
          },
          {
            "name": "department",
            "in": "query",
            "required": false,
            "description": "Case-sensitive. Any other value is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "enum": [
                "IT",
                "HR",
                "Facilities",
                "Payroll",
                "Manager",
                "Legal",
                "Finance",
                "Operations"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Missing or non-positive means 25; above 100 is clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the plan's tasks, in sequence.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OffboardingTaskSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "- `missing_parameter` — `plan_id` was not sent (\"plan_id is required\").\n- `invalid_filter` — an unknown `status` or `department` value; the\n  message names the accepted values.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `offboarding_not_enabled` — the Offboarding app is not enabled for\n  this organization.\n- `insufficient_permissions` — the app is on but not open to the caller\n  (\"You do not have access to Offboarding\"), the caller may not manage\n  offboarding, or the token lacks the scope (\"Required scope: read:offboarding\").\n- `unauthorized` — the plan exists but is outside the caller's reach\n  (for a manager, someone who is not their report).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`plan_not_found` — no plan with that `plan_id` in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/offboarding/documents": {
      "get": {
        "tags": [
          "Offboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List one offboarding plan's documents",
        "description": "The documents of one offboarding plan (separation agreement, final pay\nstub, return checklist and the rest), named by the required `plan_id`,\nin the plan's sequence (`sequence_order`, then creation order).\n\nThe caller must be able to see that plan — the same reach as\n`GET /offboarding/plans` (an admin, an Offboarding app admin or HR for\nany plan; a manager for their own reports' plans) — or the list is\nrefused with `403 unauthorized`. Each row is the document's summary\nshape; `GET /offboarding/documents/{id}` adds the file, verification\nand e-signature details.\n\nFilters, optional and combined with AND:\n- `status` — one or more of `pending`, `uploaded`, `verified`,\n  `rejected`, comma-separated.\n- `document_type` — one document type (see the parameter).\n\nAn unknown `status` or `document_type` value answers\n`400 invalid_filter` naming the accepted values, never an empty list.\n\n**Required Scopes:** `read:offboarding` (or `read:own_offboarding`, or\n`admin`). A token without it gets `403 insufficient_permissions`\n(\"Required scope: read:offboarding\"), checked after the app and\nmanage-offboarding gates. A browser or WebView session is asked no\nscope.\n",
        "parameters": [
          {
            "name": "plan_id",
            "in": "query",
            "required": true,
            "description": "The offboarding plan whose documents to list.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Comma-separated document statuses. Any value outside `pending`, `uploaded`, `verified`, `rejected` is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "example": "pending,uploaded"
            }
          },
          {
            "name": "document_type",
            "in": "query",
            "required": false,
            "description": "Any other value is a 400 `invalid_filter`.",
            "schema": {
              "type": "string",
              "enum": [
                "separation_agreement",
                "nda_reminder",
                "return_checklist",
                "exit_interview",
                "cobra_notice",
                "final_pay_stub",
                "benefits_summary",
                "exit_interview_form",
                "nda_acknowledgment",
                "benefits_information",
                "final_paycheck",
                "equipment_return",
                "reference_letter",
                "custom",
                "other"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for pagination",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Missing or non-positive means 25; above 100 is clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the plan's documents, in sequence.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OffboardingDocumentSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "- `missing_parameter` — `plan_id` was not sent (\"plan_id is required\").\n- `invalid_filter` — an unknown `status` or `document_type` value;\n  the message names the accepted values.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `offboarding_not_enabled` — the Offboarding app is not enabled for\n  this organization.\n- `insufficient_permissions` — the app is on but not open to the caller\n  (\"You do not have access to Offboarding\"), the caller may not manage\n  offboarding, or the token lacks the scope (\"Required scope: read:offboarding\").\n- `unauthorized` — the plan exists but is outside the caller's reach\n  (for a manager, someone who is not their report).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`plan_not_found` — no plan with that `plan_id` in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/offboarding/plans/{id}/cancel": {
      "patch": {
        "tags": [
          "Offboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel an offboarding plan",
        "description": "Cancels the plan and notifies the people the plan's notification\nsettings name. A plan that is already cancelled answers 200 with the\nplan and \"Offboarding plan was already cancelled\", and notifies nobody.\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:offboarding`\nscope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The offboarding plan."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why the plan was cancelled; kept in the plan's termination notes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled (or already cancelled). Returns the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "object",
                      "description": "The plan, in the detailed shape the plan's own GET returns."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `destructive:offboarding` (error code `insufficient_permissions`,\n\"Required scope: destructive:offboarding\"), the app is not enabled, or the caller\nmay not modify this plan.\n"
          },
          "404": {
            "description": "No such plan in this organization (error code `plan_not_found`)."
          },
          "422": {
            "description": "The plan could not be cancelled."
          }
        }
      }
    },
    "/offboarding/plans/{id}": {
      "delete": {
        "tags": [
          "Offboarding"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete an offboarding plan",
        "description": "Deletes the plan. Answers 200 with the deleted plan's id.\n\nDelete-level (owner decision 2026-10-04): requires the `destructive:offboarding`\nscope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The offboarding plan."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Offboarding plan deleted successfully"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks `destructive:offboarding` (error code `insufficient_permissions`),\nthe app is not enabled, or the caller may not modify this plan.\n"
          },
          "404": {
            "description": "No such plan in this organization (error code `plan_not_found`)."
          },
          "409": {
            "description": "Other records still point at the plan (error code `plan_has_dependent_records`)."
          },
          "422": {
            "description": "The plan could not be deleted."
          }
        }
      }
    },
    "/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: applicable editorial features first, then 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`posted=<id>` puts the caller's own just-published post ahead of all\nof these (see the parameter).\n\nWhen `filter=all`, the response includes a `meta.unread_counts`\nobject with per-scope unread totals (`all`, `must_read`,\n`company_news`, `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\n`filter=must_read&unacked=1` is \"still waiting on you\" — the web Must\nRead tab's `?unacked=1`: only what the caller still owes an\nacknowledgement on (the must-reads still asking them and the posts of\nthe broadcasts they owe), published and unexpired, ordered by the\nacknowledge-by deadline (`due_at`, else `expires_at`; undated last),\nthen `published_at DESC`. Read live: a post leaves the list on the next\ncall once acknowledged. `meta.counts.must_read_unacked` counts it.\n\n`q` searches the posts instead (the desktop and phone \"Search posts\",\n`NewsFeed::PostSearch`): published, unexpired posts already in the\ncaller's audience whose text matches every word, newest first, paged\nby `page` / `per_page`. A search ignores `filter`, `topic_id`,\n`category`, `segment_id`, `must_read`, `unacked` and `as_of`; its\n`meta` is the pagination plus `query` (the trimmed `q`). A blank `q`\nis the normal list; one character answers 422 `query_too_short`.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search the posts' text (at least 2 characters). See above.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "example": "fire drill"
            }
          },
          {
            "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  * `company_news` — content_category='company_news' (a CATEGORY: these\n                    posts also carry an operational or must_read priority)\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",
                "company_news",
                "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",
                "company_news",
                "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": "unacked",
            "in": "query",
            "description": "With `filter=must_read` only (ignored on every other filter):\n`1`/`true` narrows the list to what the caller still has to\nacknowledge — the same rule as the web Must Read tab's\n\"still waiting on you\", Home's Needs Acknowledgement and\n`GET /comms_hub/dashboard`'s `counts.pending_acknowledgements`.\nIncludes the post of a broadcast the caller still owes, and a\nmust-read sent to them before they moved out of its audience\n(read-only: `engageable` false, `acknowledgement_prompt` kept).\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "posted",
            "in": "query",
            "required": false,
            "description": "The id of the post the caller just published (the web Feed's\n`?posted=` after Compose). When it names a published post the\ncaller wrote in this business, that post is listed first on page 1:\ntheir own post is read for them the moment it publishes, so the\nunread-first order would otherwise put it below every unread post.\nSend it on every page of the session — each page then sorts by the\nsame keys and the post is listed once, never again on page 2+.\nIt only reorders: someone else's post, a draft, a scheduled post or\nan unknown id leaves the order unchanged (no error), and a post\noutside the list (another `filter`, `topic_id`) is not added.\nIgnored by `filter=pinned`, `filter=scheduled`, `unacked=1` and `q`.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 123
            }
          },
          {
            "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",
                          "description": "Absent on a search (`q`)."
                        },
                        "query": {
                          "type": "string",
                          "example": "fire drill",
                          "description": "Only on a search: the trimmed `q`."
                        },
                        "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"
                        },
                        "last_visit_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Page 1 only (absent on page 2+). When the caller last\nlooked at the feed before this visit — the newest\nmoment one of their posts was marked seen\n(`POST /feeds/{id}/mark_seen`, or on the web) before\n`as_of`. `null` on a first-ever visit. Marking cards\nseen during this visit records the NEXT visit; it does\nnot move this value until page 1 is requested again.\nA post is new when its `published_at` is later than\nthis and the caller is not its author.\n",
                          "example": "2026-08-30T17:42:10.123456Z"
                        },
                        "new_since_boundary_id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Page 1 only (absent on page 2+). The id of the post the\n\"New since your last visit\" divider goes UNDER, in this\npage's order — the web feed's rule: the run of new\nposts at the very top of the list. `null` for no\ndivider: no new post at the top, every post on the page\nnew, a first visit, or `filter=pinned`.\n",
                          "example": 4182
                        },
                        "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
                            },
                            "company_news": {
                              "type": "integer",
                              "example": 2
                            },
                            "operational": {
                              "type": "integer",
                              "example": 7
                            },
                            "pinned": {
                              "type": "integer",
                              "example": 1
                            },
                            "social": {
                              "type": "integer",
                              "example": 2
                            },
                            "mentions": {
                              "type": "integer",
                              "example": 4
                            }
                          }
                        },
                        "unacked": {
                          "type": "boolean",
                          "description": "Only with `filter=must_read`: whether this list is the\n`unacked=1` \"still waiting on you\" narrowing.\n"
                        },
                        "counts": {
                          "type": "object",
                          "description": "Only with `filter=must_read`.",
                          "properties": {
                            "must_read_unacked": {
                              "type": "integer",
                              "example": 3,
                              "description": "How many posts the caller still has to acknowledge —\nthe `unacked=1` list's size under the same topic,\ncategory and segment filters (the web tab's badge).\n"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "422": {
            "description": "`q` is one character (`query_too_short`: \"Type at least 2 characters to search.\").",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "query_too_short"
                        },
                        "message": {
                          "type": "string",
                          "example": "Type at least 2 characters to search."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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  * `operational` (an Announcement) or `must_read` from a caller without\n    the \"Who can post Announcements & Must-Reads\" grant is refused with\n    422 and the reason (`capabilities.can_post_announcement` /\n    `can_post_must_read` on `GET /news-feed/settings` say which a caller\n    may send). With no `priority`, the post goes out as `operational`\n    for a caller who may announce and as `social` for everyone else\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"
                        ],
                        "description": "Omitted: `operational` for a caller who may send Announcements, `social` otherwise."
                      },
                      "content_category": {
                        "type": "string",
                        "enum": [
                          "operational",
                          "company_news"
                        ],
                        "description": "Optional. `company_news` files an Operational or Must Read post under the feed's Company News tab; `operational` (the default) leaves the category to be derived from `priority`. Ignored on a Social post, which is always `social`."
                      },
                      "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)."
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional — the Communications campaign to put the post in\n(choices: `GET /comms_hub/campaigns`); null (form-encoded: an empty\nvalue) takes it out,\nand leaving it out keeps whatever campaign it is in. The\npost is saved either way: a campaign the caller may not\nuse, or one that is finished, comes back as a `warnings`\nsentence saying why it is not in it — also for someone with\nno campaign to use, and for a token without\n`write:broadcasts` (the scope that changes campaigns).\nNaming the campaign it is already in (or null for one in\nnone) changes nothing.\n"
                  },
                  "where": {
                    "$ref": "#/components/schemas/FeedWhereChoice"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present when the post was saved but not put in the campaign `campaign_id` named — says why."
                    },
                    "where_it_shows": {
                      "$ref": "#/components/schemas/FeedWhereItShowsResult"
                    }
                  }
                }
              }
            }
          },
          "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. `place_refused`:\na place ticked in `where` is not open to this person for this\naudience (the message says why), and nothing was sent.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "validation_failed",
                            "no_recipients",
                            "place_refused"
                          ],
                          "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\nWho may open it: the post's audience; before it goes out, only the\npeople preparing it. A post that has gone out also opens for the\nholder of the \"view any post\" grant (Communications › Who can view\nany post — Communications app admins by default) outside its\naudience, as the web permalink does; that read is recorded, and the\npayload carries `can_engage: false`, since the engagement endpoints\nrefuse a reader outside the audience. Its attachments, thread pages\nand poll results (`GET /feeds/{id}/media`, `/comments`,\n`/poll_summary`, `/poll_option_voters`) open for them too.\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",
            "an unsent post the caller does not prepare": null,
            "or a sent post outside the caller's audience they may not view": null
          }
        }
      },
      "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). `ai_assisted: true`\n    marks a post whose edit accepted AI-written text; on a published\n    post the mark only rises — `false` never clears it — and it does\n    not by itself stamp the post edited.\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}` (plus `ai_assisted: true`),\n`priority`, `content_category`, `allow_comments`, and a\nmust-read's acknowledgement window (`expires_at`, `due_at`).\nDraft/scheduled: full composer param set (see POST /feeds).\n",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "body": {
                        "type": "string"
                      },
                      "ai_assisted": {
                        "type": "boolean",
                        "description": "True when the edit accepted AI-written text (AI enhance). On a published post the mark only rises — false never clears it."
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "must_read",
                          "operational",
                          "social"
                        ]
                      },
                      "content_category": {
                        "type": "string",
                        "enum": [
                          "operational",
                          "company_news"
                        ],
                        "description": "Optional — company_news marks the post as company news; operational clears it. Ignored on social posts."
                      },
                      "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,
                        "description": "When the post stops asking (and, for a post that asks\nno acknowledgement, when it leaves the feed). On a\npublished must-read with a separate `due_at`, sent\nalone it moves only this end — unless it lands before\n`due_at`, which then moves to it. The stored value sent\nback unchanged moves nothing. Send `due_at` with it to\nset each end.\n"
                      },
                      "due_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true,
                        "description": "Published must-reads only (ignored on a post that asks\nno acknowledgement). Sent alone, the new\nacknowledge-by moment — `expires_at` moves with it,\nkeeping the extra asking days; sent with `expires_at`,\neach end as sent (a `due_at` after `expires_at` is\nrefused with 422).\n"
                      },
                      "requires_acknowledgement": {
                        "type": "boolean"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "scheduled",
                          "published"
                        ]
                      }
                    }
                  },
                  "author_topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic names the author attached without a literal `#name`\nin the body. Added to the post; on their own they never\nremove a topic (see `author_topics_listed`).\n"
                  },
                  "rejected_topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic names the author dismissed in the composer suggestion strip (PRD 11)."
                  },
                  "author_topics_listed": {
                    "type": "boolean",
                    "description": "Draft and scheduled posts only. True when the client showed\nthe post's own author topics and sends all of them in\n`author_topics` (an empty list included): an author topic\nleft out — and not a `#hashtag` still in the body — is\nremoved. Topics the AI assigned are never removed. A post\nwaiting for approval whose topics change starts its review\nagain, under the workflow that now applies to it (a review\nopened before topics counted toward approval is left as it is).\n"
                  },
                  "campaign_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optional — the Communications campaign to put the post in\n(choices: `GET /comms_hub/campaigns`); null (form-encoded: an empty\nvalue) takes it out,\nand leaving it out keeps whatever campaign it is in. The\npost is saved either way: a campaign the caller may not\nuse, or one that is finished, comes back as a `warnings`\nsentence saying why it is not in it — also for someone with\nno campaign to use, and for a token without\n`write:broadcasts` (the scope that changes campaigns).\nNaming the campaign it is already in (or null for one in\nnone) changes nothing.\n"
                  },
                  "where": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/FeedWhereChoice"
                      }
                    ],
                    "description": "Draft or scheduled posts only (`where_not_editable` on a sent\none). Sent, it decides the post's Top of Home and store pin:\na place it leaves unticked is taken off. Left out, the\nplaces the post already holds stay as they are.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/NewsFeedDetail"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present when the post was saved but not put in the campaign `campaign_id` named — says why."
                    },
                    "where_it_shows": {
                      "$ref": "#/components/schemas/FeedWhereItShowsResult"
                    }
                  }
                }
              }
            }
          },
          "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 (`validation_failed`), or `demo_question` — an\nunsent sample-data demo question, which is posted only from its\nexample (on the web, in Compose) on the example's terms.\n`place_refused`: a place ticked in `where` is not open to this\nperson for this audience, and nothing was saved.\n`where_not_editable`: `where` sent on a post already out.\n",
            "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\nDelete-level (#6720 / #6721): requires the `destructive:news_feed` scope.\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"
                    },
                    "message": {
                      "type": "string",
                      "example": "You won't get notifications about this post.",
                      "description": "Confirmation for the app's toast, in the reader's language; on a must-read the reader still owes it adds that they still need to acknowledge it."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "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 200 with a confirmation message even if no mute existed.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mute removed (or none existed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Notifications unmuted",
                      "description": "Confirmation for the app's toast."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "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\nDelete-level (#6720 / #6721 — a close that `/reopen_discussion` takes\nback still asks the delete scope, #6768): requires the\n`destructive:news_feed` scope. Reopening asks `write:news_feed`.\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 may not send it: not the author or a Communications admin;\nor the post is being written by a delegate for someone else, which\nonly its author and that delegate send.\n"
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "Feed is not in `scheduled` state (`not_scheduled`), or it is an\nunsent sample-data demo question (`demo_question`).\n",
            "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 in the post's audience can pin a live post — `can_pin` on the\npost says so. A post outside the caller's\naudience answers 404, the same as one that does not exist. A draft or\nscheduled post answers 422 `not_published` to the people preparing\nit — nobody pins a post before it goes out — and 404 to everyone else.\nAn expired or archived post answers 422 `not_pinnable`: the pinned\nlist shows only published, unexpired posts. Unpin stays open.\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": "The post is listed for the caller but they may not open it"
          },
          "404": {
            "description": "Feed not found",
            "or outside the caller's audience": null
          },
          "422": {
            "description": "`not_published` — a draft or scheduled post, to someone preparing it; `not_pinnable` — an expired or archived post"
          }
        }
      },
      "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, and open on an expired or\narchived post, so a pin that went stale can be removed.\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"
          },
          "422": {
            "description": "`not_published` — a draft or scheduled post, to someone preparing it"
          }
        }
      }
    },
    "/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\nThe company's moderation settings apply. A comment held for review\ncomes back 201 with `held_for_review: true`: only its author is ever\nsent it, until a moderator approves it. Send `moderation_nudge: true`\nto stop first instead: flagged words answer 422 with\n`error.code: moderation_nudge` and `error.moderation_nudge` (`reason`\nto show the author, `token`, `can_send_for_review`), and nothing is\nsaved. The same words sent again with `moderation_nudge_token` are\nheld for a moderator; changed words are screened from the start.\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"
                  },
                  "moderation_nudge": {
                    "type": "boolean",
                    "description": "This client can show the author why their words were flagged."
                  },
                  "moderation_nudge_token": {
                    "type": "string",
                    "description": "The token from a moderation_nudge answer, sent with the same words."
                  },
                  "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_closed` — the post takes no comments: written with\ncomments off, its discussion closed, or a recognition's mirrored\npost whose comments live on the recognition (the post's\n`comments_allowed` is false; `social_thread` names where). Also\n`comments_disabled` / `discussion_layer_disabled` when the tenant\nturned comments or the discussion layer off.\n"
          },
          "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": "React to a feed (set, swap or toggle off)",
        "description": "At most one reaction per user per feed. POSTing an emoji when the\ncaller has none adds it; a different emoji swaps it in place; the\nSAME emoji removes it (a toggle — `reaction` is then null). Every\nbranch answers 200 with the feed's refreshed `aggregate`. DELETE\nremoves the caller's reaction whatever its emoji.\n\nThe emoji must be in the business's reaction palette, except a\nreaction the caller already holds, which can always be toggled off.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. The sign-in tokens and a token made\nby hand hold it; a browser session is asked no scope.\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": "The caller's reaction after the tap, and the feed's aggregate",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reaction",
                    "aggregate"
                  ],
                  "properties": {
                    "reaction": {
                      "allOf": [
                        {
                          "type": "object",
                          "description": "One person's reaction on a post or a comment; exactly one of `feed_id` / `comment_id` is set.",
                          "required": [
                            "id",
                            "feed_id",
                            "comment_id",
                            "emoji_key",
                            "tier",
                            "user_id",
                            "reacted_at",
                            "created_at"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "feed_id": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Set for a reaction on a post; null on a comment's."
                            },
                            "comment_id": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Set for a reaction on a comment; null on a post's."
                            },
                            "emoji_key": {
                              "$ref": "#/components/schemas/NewsFeedEmojiKey"
                            },
                            "tier": {
                              "type": "string",
                              "enum": [
                                "expressive",
                                "action_oriented"
                              ]
                            },
                            "user_id": {
                              "type": "integer"
                            },
                            "reacted_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the reaction was set or last swapped."
                            },
                            "created_at": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        }
                      ],
                      "nullable": true,
                      "description": "Null when the tap toggled the caller's reaction off."
                    },
                    "aggregate": {
                      "$ref": "#/components/schemas/NewsFeedReactionsAggregate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "- `invalid_emoji` — \"Emoji is not in the active palette\".\n- `not_published` — \"This post hasn't been published yet.\": the post is a draft or scheduled, and the caller is preparing it.\n- `reactions_closed` — \"Reactions are off for this post.\": the post takes no reactions (`reactions_enabled` false); taking your own off still works.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remove the caller's reaction on a feed",
        "description": "Removes the caller's reaction from the post, whatever its emoji, and\nsends the new count to everyone viewing it, as adding one does.\nAnswers `204` whether or not there was one.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. The sign-in tokens and a token made\nby hand hold it; a browser session is asked no scope.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Reaction removed (or already absent — idempotent)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          },
          "422": {
            "description": "`not_published` — \"This post hasn't been published yet.\": the post is a draft or scheduled, and the caller is preparing it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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/{feed_id}/comments/{id}/not_solved": {
      "parameters": [
        {
          "in": "path",
          "name": "feed_id",
          "required": true,
          "description": "The Question post's id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "in": "path",
          "name": "id",
          "required": true,
          "description": "The id of the expert's reply — a top-level comment on that post whose `expert_answer.can_mark_not_solved` is true.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Tell an expert's reply \"This didn't solve it\"",
        "description": "The asker's other answer to \"Did this solve it?\" under a reply from an\nexpert their question was sent to (\"Mark as correct\" is\n`POST /feeds/{id}/correct_answer`). Records it on that reply — every\nreader of the post then sees `expert_answer.state: not_solved` on it —\nasks the next expert now where there is one, and tells the expert\nwhose reply it was. Only the person who asked the question may say\nit, once per expert, while the discussion is open and no answer is\nmarked correct. Takes no body.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "expert_answer"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "The sentence to show the asker — what happened next (another expert was asked, or why nobody was).",
                      "example": "Thanks for telling us — we've asked another expert."
                    },
                    "expert_answer": {
                      "$ref": "#/components/schemas/NewsFeedExpertAnswer"
                    }
                  }
                },
                "example": {
                  "message": "Thanks for telling us — we've asked another expert.",
                  "expert_answer": {
                    "state": "not_solved",
                    "can_mark_correct": false,
                    "can_mark_not_solved": false
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `not_allowed` \"Only the person who asked the question can say this.\"\n- `not_available` \"This isn't available for this question.\" — for one, the company has correct answers switched off.\n- `discussion_layer_disabled` — discussions are switched off for the business.\n- `not_authorized` \"You do not have access to this feed\" — the caller is not in the post's audience.\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` \"Feed not found\" — no such post in the business, or a draft or scheduled post the caller is not preparing.\n- `comment_not_found` \"Comment not found on this feed\" — no comment with this id on that post.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Nothing was recorded; `error.message` is the sentence to show.\n- `already_not_solved` \"You've already told us this didn't solve it.\"\n- `already_answered` \"This question already has a correct answer.\"\n- `discussion_closed` \"The discussion on this question is closed.\"\n- `question_ended` \"This question has expired or been archived, so no other expert can be asked.\"\n- `comment_deleted` \"That reply is no longer available.\"\n- `not_an_expert_reply` \"This reply isn't from an expert the question was sent to.\" — also a reply under a comment.\n- `not_published` — the post has not gone out yet (only its preparers get this).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          },
          "422": {
            "description": "`not_published` — a draft or scheduled post, to someone preparing it: nothing is recorded before it goes out"
          }
        }
      },
      "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/where_it_shows": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The phone composer's \"Where it shows\" places",
        "description": "The places this person may put a post with this audience — the /m/\ncomposer's card, from the same code (`Comms::PhoneWhereItShows`). News\nand Inbox always apply and are not listed. \"Skip News and Inbox\" is a\ndesktop-only choice and is never offered here. Send the ticks back as\n`where` on `POST /feeds` or `PATCH /feeds/{id}`; they are checked again\nthere for the audience the post is sent with.\n\nTakes the audience fields of `POST /feeds` as query parameters —\n`feed[audience_type]`, `feed[audience_segment_ids][]`,\n`feed[audience_criteria]` (JSON), `feed[extra_user_ids][]`; none means\neveryone. A selection that reaches nobody offers no place, and neither\ndoes a token without `write:news_feed` — the scope `POST /feeds` and\n`PATCH /feeds/{id}` ask to book one.\n",
        "parameters": [
          {
            "name": "draft_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A reopened draft or scheduled post the caller may edit (its author,\nor an app admin sending a teammate's) — the places it already holds\ncome back `checked`. 404 `not_found` for one the caller can't edit,\nor that has been sent or deleted.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "The places offered; a null place is not offered to this person for this audience.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "top_of_home": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "checked": {
                          "type": "boolean"
                        },
                        "note": {
                          "type": "string",
                          "description": "When it would start for these people (Top of Home runs 7 days from the first free start)."
                        }
                      }
                    },
                    "store_pin": {
                      "type": "object",
                      "nullable": true,
                      "description": "Offered when everyone the post reaches works at one store the author manages.",
                      "properties": {
                        "checked": {
                          "type": "boolean"
                        },
                        "label": {
                          "type": "string",
                          "example": "Pin on Store 214's home until"
                        },
                        "default_until": {
                          "type": "string",
                          "format": "date"
                        },
                        "days": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "date"
                          },
                          "description": "The days the pin may run to — today and the six after."
                        },
                        "replaces": {
                          "type": "string",
                          "nullable": true,
                          "description": "The pin it replaces, e.g. \"Replaces “Truck moved to 10:00 AM”, pinned yesterday.\""
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Composer disabled for this business, or the token lacks `read:news_feed`"
          },
          "404": {
            "description": "`draft_id` names a post the caller cannot edit, or one already sent or deleted (`not_found`)."
          },
          "503": {
            "description": "The places could not be checked (`where_it_shows_unavailable`) — say so, never \"no places\"."
          }
        }
      }
    },
    "/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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/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, everyone counted (equals total_count unless a works council hides some viewers — see names_hidden); only the named viewers while names_hidden.count is null."
                        },
                        "total_count": {
                          "type": "integer",
                          "description": "Viewers in the named list."
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present only when a works council hides names (General\nSettings › Countries): the viewers left out of `viewers`\n— counted, never named.\nWhile the hidden group is under the company's smallest reportable\ngroup, `count` and `group_count` are null and `fewer_than` is that\ngroup, \"a few\" (the status counts beside it count only the named\npeople) — the shape of `BroadcastNamesHidden`.\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "group_count": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                        },
                        "fewer_than": {
                          "type": "integer",
                          "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "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"
                      ],
                      "description": "Pages the NAMED list; people a works council hides are counted in `names_hidden`.",
                      "properties": {
                        "total_count": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "current_page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present only when a works council hides names (General\nSettings › Countries): the people left out of `users`\n— counted, never named.\nWhile the hidden group is under the company's smallest reportable\ngroup, `count` and `group_count` are null and `fewer_than` is that\ngroup, \"a few\" (the status counts beside it count only the named\npeople) — the shape of `BroadcastNamesHidden`.\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "group_count": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                        },
                        "fewer_than": {
                          "type": "integer",
                          "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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  * A soft-deleted comment — including a reply deleted with its\n    parent — answers 404, as a missing one does.\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, or the content-editing flag is off.\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 or soft-deleted"
          },
          "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  * Deleting a top-level comment deletes its replies with it (owner\n    decision #7810) — the audit row's `replies_removed` says how many.\n    The replies go in a background job seconds after the response; a\n    reply deleted with its parent then answers 404 here like any\n    deleted comment.\n\nReturns 204 on success. Delete-level (#6720 / #6721): requires the\n`destructive:news_feed` scope.\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\nDelete-level (#6720 / #6721): requires the `destructive:news_feed` scope.\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
                        },
                        "can_vote": {
                          "type": "boolean",
                          "description": "Whether POST (a first vote) or PATCH (a change) on\n`/feeds/{id}/poll_votes` would take this caller's ballot now — the\ndoor's own rule: never through an impersonation token or under\nView-as, on a draft, a closed poll, a vote that may not change, or\nfor a reader outside the audience. Draw the ballot only when true.\n"
                        },
                        "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"
                        },
                        "voters_visible": {
                          "type": "boolean",
                          "description": "True when `GET /feeds/{id}/poll_option_voters` answers this caller\n— draw \"See who voted\" exactly then. Always false on an anonymous\npoll (`is_anonymous`); on a named poll it follows the\n`results_visible` rules above. The same rule as the web and phone\n\"See who voted\" and the privacy sentence under every poll.\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
                        },
                        "can_vote": {
                          "type": "boolean",
                          "description": "Whether POST (a first vote) or PATCH (a change) on\n`/feeds/{id}/poll_votes` would take this caller's ballot now — the\ndoor's own rule: never through an impersonation token or under\nView-as, on a draft, a closed poll, a vote that may not change, or\nfor a reader outside the audience. Draw the ballot only when true.\n"
                        },
                        "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"
                        },
                        "voters_visible": {
                          "type": "boolean",
                          "description": "True when `GET /feeds/{id}/poll_option_voters` answers this caller\n— draw \"See who voted\" exactly then. Always false on an anonymous\npoll (`is_anonymous`); on a named poll it follows the\n`results_visible` rules above. The same rule as the web and phone\n\"See who voted\" and the privacy sentence under every poll.\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
                        },
                        "can_vote": {
                          "type": "boolean",
                          "description": "Whether POST (a first vote) or PATCH (a change) on\n`/feeds/{id}/poll_votes` would take this caller's ballot now — the\ndoor's own rule: never through an impersonation token or under\nView-as, on a draft, a closed poll, a vote that may not change, or\nfor a reader outside the audience. Draw the ballot only when true.\n"
                        },
                        "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"
                        },
                        "voters_visible": {
                          "type": "boolean",
                          "description": "True when `GET /feeds/{id}/poll_option_voters` answers this caller\n— draw \"See who voted\" exactly then. Always false on an anonymous\npoll (`is_anonymous`); on a named poll it follows the\n`results_visible` rules above. The same rule as the web and phone\n\"See who voted\" and the privacy sentence under every poll.\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 `users` / `grouped` lists stop at 200 voters per option; `total`\ncounts the option's whole roster. The poll block's `voters_visible`\nsays whether this endpoint answers the caller.\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.\nWhen someone reports to the caller, the list 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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/news-feed/people": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "People a post can be aimed at (composer \"Pick people\")",
        "description": "The composer's \"Pick people\" list for a post aimed at specific people.\nSend the picked `id`s back as `feed[extra_user_ids]` on `POST /feeds`.\n\nNeeds only the Communications app (and `read:news_feed`) — not the\nPerformance app's feedback picker, which earlier clients paged. The\nlist is the web composer's people picker: active members of this\ncompany, no service accounts, the caller included (`is_self: true`).\n`q` matches names, and email, job title, office, phone or employee id only\nwhere the People directory shows that field to the caller; `email`\nand `meta` are null/blank where it does not.\n\nPaged in a fixed order, so no person repeats or goes missing across\npages: read `meta.has_next_page`.\n\nComposer-gated: a caller who can neither compose nor post a must-read\ngets 403.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search term. Blank lists everyone, alphabetically."
          },
          {
            "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": "One page of people",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "meta"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "is_self"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "example": 4521
                          },
                          "name": {
                            "type": "string",
                            "example": "Dana Ortiz"
                          },
                          "email": {
                            "type": "string",
                            "nullable": true,
                            "description": "Null where the directory hides email from the caller."
                          },
                          "meta": {
                            "type": "string",
                            "nullable": true,
                            "example": "Shift Lead · Denver",
                            "description": "What the directory shows the caller of this person (title, office, email)."
                          },
                          "detail": {
                            "type": "string",
                            "nullable": true,
                            "description": "The second line that tells namesakes apart."
                          },
                          "is_self": {
                            "type": "boolean",
                            "description": "True on the caller's own row."
                          },
                          "avatar_url": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "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"
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "unread_notification_count": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Communications is off or closed to the caller\n(`CommunicationsUnavailable`), the token lacks `read:news_feed`, or\nthe caller cannot compose.\n"
          }
        }
      }
    },
    "/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"
                          },
                          "following": {
                            "type": "boolean",
                            "description": "Whether the caller follows this topic (followed topics lead their feed)."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/news-feed/topics/{id}/follow": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The topic's id (any topic of the caller's business, active or retired).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Follow a topic",
        "description": "Follows the topic for the caller. Idempotent — following a followed\ntopic changes nothing. Requires the `write:news_feed` scope. The same\nwriter serves the web and phone Follow control (`NewsFeed::TopicFollow.follow!`).\n",
        "responses": {
          "200": {
            "description": "The topic, now followed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedTopicFollowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "404": {
            "description": "Topic not found"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Unfollow a topic",
        "description": "Stops following the topic. Idempotent. Requires the `write:news_feed` scope.\n",
        "responses": {
          "200": {
            "description": "The topic, no longer followed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedTopicFollowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "404": {
            "description": "Topic not found"
          }
        }
      }
    },
    "/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  * `drafts`    — not sent yet (`status=draft`), minus the composer's\n                  body-less scaffolding posts and anything at an\n                  approval gate — the web Drafts tab; its posts are\n                  the phone's My drafts. Alias: `draft`.\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\nEach row also says what the web row says: on `approval`, who it waits\non (`waiting_on_label`, `waiting_phrase`); on `drafts`, who turned it\ndown and why (`not_approved`); on `sent`, how many people it went to\n(`published_to_count`) and read it (`read_count`). Each is `null` on\nrows it does not describe.\n\n`meta.filter_counts` carries the per-filter totals (drafts / scheduled /\napproval / sent) so the client can render the tab badges without extra\nround-trips. They count the lifecycle, not the search: `q` and `type`\nnarrow the list and `meta.total`, never the badges — as on the web.\n\n`q` and `type` are the web page's search box and Type chips, composed\nwith `filter`: for the same author, words and type the endpoint returns\nthe same rows and count as web My Posts.\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": [
                "drafts",
                "draft",
                "scheduled",
                "approval",
                "awaiting_approval",
                "sent",
                "published"
              ],
              "default": "sent"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search text, matched anywhere (case-insensitive) in a post's body\nor a broadcast's title and description. At least 2 characters —\na shorter one answers 422 `query_too_short`. Blank means no search.\n",
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "The web Type chip: a message kind (`post`, `announcement`,\n`must_read`, `break_through` — a Broadcast — or `in_context` — Spot\nonly), or `all` (the default). `broadcast` is the old value for\n`break_through`. Anything else answers 422 `invalid_type`.\n`meta.types` lists the chips with their labels.\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "post",
                "announcement",
                "must_read",
                "break_through",
                "in_context",
                "broadcast"
              ],
              "default": "all"
            }
          },
          {
            "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": "The post's body as written (Markdown) / the broadcast's description. A post stored as HTML (a newsletter's in-app copy) is written out as Markdown, with its pictures at addresses signed for an hour — the same `body` GET /feeds serves."
                          },
                          "newsletter": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/NewsFeedNewsletterCopy"
                              }
                            ],
                            "nullable": true,
                            "description": "A newsletter's in-app copy cut at its hero banners — the same `newsletter` GET /feeds serves; null on every other row."
                          },
                          "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`."
                          },
                          "left_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Of the people still to acknowledge, those who left the company first — they\nnever can. The post is done once `acknowledged_count + left_count` reaches\n`recipient_count` (show \"N left before acknowledging\"). `null` unless ack-gated on `sent`.\n"
                          },
                          "acknowledgement_percent": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Rounded `acknowledged_count / recipient_count` percentage — the bar's width. `null` unless ack-gated on `sent`."
                          },
                          "acknowledgement_percent_label": {
                            "type": "string",
                            "nullable": true,
                            "example": "<1%",
                            "description": "The percentage as words to draw beside the counts: whole numbers, except a\nnon-zero count that rounds to 0 reads `<1%` and a count short of the\naudience that rounds to 100 reads `>99%` — so `1/500` never reads `0%`.\n`null` exactly when `acknowledgement_percent` is.\n"
                          },
                          "names_hidden": {
                            "type": "object",
                            "description": "Present only on a row whose works-council group (General\nSettings › Countries) is under the company's smallest reportable\ngroup: `acknowledged_count`, `recipient_count` and\n`acknowledgement_percent` then count only the named people, and\nthe group is not counted — `count` and `group_count` are null and\n`fewer_than` is that group, \"a few\" (the shape of `BroadcastNamesHidden`).\n",
                            "properties": {
                              "count": {
                                "type": "integer",
                                "nullable": true
                              },
                              "group_count": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                              },
                              "fewer_than": {
                                "type": "integer",
                                "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                              },
                              "reason": {
                                "type": "string",
                                "example": "Names are hidden for people in Germany (works council)."
                              }
                            }
                          },
                          "published_to_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "How many people a sent post went to — the acknowledgement audience for a post\nthat asks for it, else the audience the engagement figures count. `null` off\n`filter=sent`, and before a post's first engagement refresh (~15 min).\n"
                          },
                          "read_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Of `published_to_count`, how many read it (the web row's \"Read by N of M\") — for a\nsent post that asked nobody to acknowledge it. `null` otherwise.\n"
                          },
                          "approval_request_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The pending approval request behind an `approval` row — the id POST `/approvals/{id}/withdraw` takes. `null` on other rows."
                          },
                          "waiting_on_label": {
                            "type": "string",
                            "nullable": true,
                            "example": "anyone with the manager role",
                            "description": "Who it waits on, in the web row's words (\"Waiting on …\"). `null` off `approval`."
                          },
                          "waiting_phrase": {
                            "type": "string",
                            "nullable": true,
                            "example": "for 2 days",
                            "description": "How long it has waited (\"for 2 days\", \"for 3 hours past its deadline\"). `null` off `approval`."
                          },
                          "not_approved": {
                            "type": "object",
                            "nullable": true,
                            "description": "A draft whose latest review turned it down — who, and the reason they gave. `null` otherwise.",
                            "properties": {
                              "approval_request_id": {
                                "type": "integer"
                              },
                              "by": {
                                "type": "string",
                                "nullable": true,
                                "description": "The reviewer's name."
                              },
                              "notes": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "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",
                        "q",
                        "type",
                        "total",
                        "types"
                      ],
                      "properties": {
                        "filter": {
                          "type": "string",
                          "enum": [
                            "drafts",
                            "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 — the lifecycle's, before `q` and `type`.",
                          "properties": {
                            "drafts": {
                              "type": "integer"
                            },
                            "scheduled": {
                              "type": "integer"
                            },
                            "approval": {
                              "type": "integer"
                            },
                            "sent": {
                              "type": "integer"
                            }
                          }
                        },
                        "q": {
                          "type": "string",
                          "nullable": true,
                          "description": "The search applied, trimmed; `null` when none."
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "all",
                            "post",
                            "announcement",
                            "must_read",
                            "break_through",
                            "in_context"
                          ],
                          "description": "The Type applied (`broadcast` is reported as `break_through`)."
                        },
                        "total": {
                          "type": "integer",
                          "description": "How many posts match the filter, search and type — the web's result count. Equal to `total_count`."
                        },
                        "types": {
                          "type": "array",
                          "description": "The Type chips in the web's order, with their labels.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string",
                                "example": "must_read"
                              },
                              "label": {
                                "type": "string",
                                "example": "Must-read"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Communications not enabled or not accessible for this business"
          },
          "422": {
            "description": "`query_too_short` — `q` is under 2 characters (`details.min_length`);\n`invalid_type` — `type` names no Type (`details.allowed`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "query_too_short",
                            "invalid_type"
                          ]
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/team-must-reads": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "My team's must-reads (who on the caller's team still owes an acknowledgement)",
        "description": "The phone page \"My team's must-reads\" for the native apps\n(`Comms::TeamMustReads`): who on the caller's team still owes an\nacknowledgement on a must-read post or an acknowledgement broadcast,\nmost overdue first, people first. The team is this business's reporting\nline. `reach=all` (everyone below the caller) is a communicator's view —\na company admin, a Communications app admin, a manager or above, or\nsomeone in a leadership position (`reach_all_allowed`); anyone else\nasking for it gets their direct reports, as on the page. Under a works\ncouncil the hidden people are left off `people` and reported the way\nthe other status lists report them (never a small group's exact size).\nSomeone with no team gets 200 with `has_team: false` and a message.\nRead-only.\n",
        "parameters": [
          {
            "name": "reach",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "direct",
                "all"
              ],
              "default": "direct"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "outstanding",
                "overdue"
              ],
              "default": "outstanding"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "People listed (default 100, at most 200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The team's owed must-reads, or has_team false.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "has_team": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "description": "Only when has_team is false."
                    },
                    "reach": {
                      "type": "string",
                      "enum": [
                        "direct",
                        "all"
                      ]
                    },
                    "reach_all_allowed": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "outstanding",
                        "overdue"
                      ]
                    },
                    "team_size": {
                      "type": "integer"
                    },
                    "people_owing": {
                      "type": "integer"
                    },
                    "outstanding": {
                      "type": "integer"
                    },
                    "overdue": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "More people owe than `limit` lists."
                    },
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              }
                            }
                          },
                          "pending": {
                            "type": "integer"
                          },
                          "overdue": {
                            "type": "integer"
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string",
                                  "enum": [
                                    "NewsFeed::Feed",
                                    "Broadcast"
                                  ]
                                },
                                "id": {
                                  "type": "integer"
                                },
                                "kind": {
                                  "type": "string",
                                  "example": "Must-read"
                                },
                                "title": {
                                  "type": "string"
                                },
                                "due_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "overdue": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present only when a works council hides names (General\nSettings › Countries): the people owing who are left out of\n`people` — counted, never named.\nWhile the team's hidden group is under the company's smallest\nreportable group, `count` and `group_count` are null and\n`fewer_than` is that group, \"a few\" (the figures beside it\ncount only the named people) — the shape of `BroadcastNamesHidden`.\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "group_count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "fewer_than": {
                          "type": "integer",
                          "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/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"
                        }
                      }
                    },
                    "compose_menu": {
                      "type": "object",
                      "description": "The web's \"New communication\" menu: the kinds this\nperson may send, named by intent, and their saved\npresets — only the kinds this token could send.\n",
                      "properties": {
                        "kinds": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "type_label": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "presets": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "kind": {
                                "type": "string"
                              },
                              "type_label": {
                                "type": "string"
                              },
                              "summary": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "emergency_alert": {
                          "type": "boolean",
                          "description": "Offer the menu's separate Emergency alert entry. Never\none of `kinds`, so nobody reaches an alert by\naccident. True only for someone the alert page lets\nin and lets send (Safety Hub open to them, its alerts\nmodule on, a manager or the app's admin, and allowed\nto dispatch), on a token holding `write:broadcasts` —\nthe scope `POST /alerts` asks.\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\nA channel the company switched off (Knowledge Layer P5)\nis still listed, with `off_for_company: true` and its\n`hint` saying so — it sends a non-Critical broadcast\nnothing, so draw it as off rather than offer it as if it\nwent.\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\none. For a channel that is `off_for_company`, the\nweb composer's own note: the sentence, where an\nadmin turns it back on, and for email and voice that\na Critical broadcast still uses it.\n",
                            "example": "Voice call is off for your company — Broadcast settings. A Critical broadcast still uses it."
                          },
                          "off_for_company": {
                            "type": "boolean",
                            "description": "`email` / `sms` / `voice` only. True when the company\nhas switched this channel off (its notification /\nSMS / Broadcast settings), so a non-Critical broadcast\nsends nothing on it. A Critical broadcast still goes\nout by email and voice call; SMS stays off.\n"
                          },
                          "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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/admin/news-feed/settings": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Communications settings, for an admin",
        "description": "Every Communications setting the tenant can change, as stored: a saved\nchoice, else the setting's default. The two reader switches\n(`show_ai_assisted_to_readers`, `show_delegate_to_readers`) are\ncompany-wide and come from their one home, the same values Admin ›\nContent Moderation › Settings shows.\n\nFor a company admin or a Communications app admin. A token also needs\nthe `read:settings` scope (`admin` and `read:all` include it); a\nsigned-in session is not asked for a scope.\n",
        "responses": {
          "200": {
            "description": "The settings, keyed by setting name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settings"
                  ],
                  "properties": {
                    "settings": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "The organization has used its plan's monthly API allowance and has no API credits left\n(error code `api_credits_exhausted`). Answered to a call made with an API token someone\ncreated (personal, admin, service-account or CLI) until the allowance resets on the 1st,\ncredits are bought, or auto top-up buys more; never to the MangoApps apps' own sign-in or a\nsigned-in session.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "api_credits_exhausted",
                    "message": "This account has used its plan's API allowance for the month and has no API credits left. Buy platform credits, or turn on auto top-up, under Admin → Billing & Payments → Credits; the allowance resets on the 1st.",
                    "details": {
                      "buy_credits": "/admin/billing/purchase_credits?resource_type=api_calls"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an admin (`{\"error\": {\"code\": \"forbidden\", \"message\": \"Admin access required.\"}}`), or the token\nlacks the scope (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Insufficient permissions. Required scope: read:settings\"}, \"code\": \"insufficient_permissions\"}`).\n"
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Change Communications settings, for an admin",
        "description": "Saves the settings sent, all or nothing, with the checks Communications\n› Settings applies (a choice from the listed options, a number within\nrange, a valid reaction list); keys the Communications app does not\ndeclare are ignored. The two reader switches are written to their\ncompany-wide home. The Comms Hub module switches and Broadcast's AI and\nchannel defaults are set on their own pages and refused here when\nchanged. Only what changed is written: a setting sent with the value it\nalready has (as `GET` returned it) is left as it is, so sending back the\nwhole `settings` object with one value edited saves just that one.\n\nFor a company admin or a Communications app admin. A token also needs\nthe `write:settings` scope (`admin` includes it; `read:settings` and\n`read:all` do not); a signed-in session is not asked for a scope.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "settings"
                ],
                "properties": {
                  "settings": {
                    "type": "object",
                    "additionalProperties": true,
                    "example": {
                      "show_ai_assisted_to_readers": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The setting names that were saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "The organization has used its plan's monthly API allowance and has no API credits left\n(error code `api_credits_exhausted`). Answered to a call made with an API token someone\ncreated (personal, admin, service-account or CLI) until the allowance resets on the 1st,\ncredits are bought, or auto top-up buys more; never to the MangoApps apps' own sign-in or a\nsigned-in session.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "api_credits_exhausted",
                    "message": "This account has used its plan's API allowance for the month and has no API credits left. Buy platform credits, or turn on auto top-up, under Admin → Billing & Payments → Credits; the allowance resets on the 1st.",
                    "details": {
                      "buy_credits": "/admin/billing/purchase_credits?resource_type=api_calls"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an admin (`{\"error\": {\"code\": \"forbidden\", \"message\": \"Admin access required.\"}}`), or the token\nlacks the scope (`{\"error\": {\"code\": \"insufficient_permissions\", \"message\": \"Insufficient permissions. Required scope: write:settings\"}, \"code\": \"insufficient_permissions\"}`).\n"
          },
          "422": {
            "description": "Nothing was saved: a value Communications › Settings would refuse\n(`{\"error\": {\"code\": \"validation_failed\", \"message\": \"Invalid value for signal_threshold_risk — must be at most 1.0.\"}}`),\nor a changed setting kept on another page (`{\"error\": {\"code\": \"validation_failed\", \"message\": \"enable_calendar: set on Communications › Settings › Modules or Broadcast › Settings, not through this endpoint.\"}}`).\n"
          }
        }
      }
    },
    "/notification-preferences": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Your Communications notification cadence",
        "description": "How the caller hears about new Communications posts in this business:\nthe default preference (`segment_id: null`) first, then any\nper-segment preferences.\n\nSomeone who never saved a preference still gets a default entry, with\n`persisted: false` — the cadence they actually get, so a settings\nscreen can say \"Announcements: hourly digest\" instead of nothing.\nPreferences are per business: a person in two businesses has one set\nin each.\n\nMust-read posts always notify right away, whatever the cadence.\n",
        "responses": {
          "200": {
            "description": "The caller's preferences in this business",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "preferences"
                  ],
                  "properties": {
                    "preferences": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedNotificationPreference"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Communications is not enabled for this business",
            "or the caller has no access to it": null
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Change your Communications notification cadence",
        "description": "Saves the caller's cadence in this business. Leave `segment_id` out for\nthe default preference. A field left out keeps its current value; an\nempty `operational_frequency` resets it to the company default.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "segment_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "A notification segment of this business; omit for the default preference"
                  },
                  "operational_frequency": {
                    "type": "string",
                    "enum": [
                      "realtime",
                      "hourly",
                      "daily",
                      ""
                    ],
                    "description": "Announcements — right away, or an hourly or daily digest. Empty resets to the company default."
                  },
                  "social_frequency": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "weekly"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved preference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedNotificationPreference"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Communications is not enabled for this business",
            "or the caller has no access to it": null
          },
          "422": {
            "description": "An unknown segment (`{\"error\": {\"code\": \"validation_failed\", \"message\": \"Unknown segment.\"}}`) or an invalid\nfrequency (`{\"errors\": [\"Operational frequency is not included in the list\"]}`).\n"
          }
        }
      },
      "put": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Change your Communications notification cadence (same as PATCH)",
        "responses": {
          "200": {
            "description": "The saved preference"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/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
          }
        }
      }
    },
    "/feeds/{id}/feedback": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send the post's author private feedback",
        "description": "A private note to the post's AUTHOR and no one else, always sent under\nthe caller's name (owner decision 2026-10-16). Only the sender and the\nauthor ever see it. Asks `write:news_feed`; the caller must be in the\npost's audience and the post must have gone out (the same door as\n`POST /feeds/{id}/reactions`). Refused with 422 and one of these codes:\n`not_published`, `own_post` (your own post), `no_author`,\n`author_left` (the author is no longer a member), `already_open` (your\nearlier feedback on this post is still open), `invalid` (blank or over\n2,000 characters).\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
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/NewsFeedPostFeedback"
                    },
                    "message": {
                      "type": "string",
                      "example": "Feedback sent. Only Grace Lee can see it."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`not_authorized` — the caller is not in the post's audience; or Communications is unavailable to them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found.\" — no such post, or a draft the caller may not read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Refused — see the codes above.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feedback": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The caller's private feedback on posts (Received or Sent)",
        "description": "`box=received` — feedback readers sent the caller about their posts\n(default); `box=sent` — feedback the caller sent other authors. Only\nthe caller's own rows, newest first. Asks `read:news_feed`; the `can_*`\nflags are false for a token without `write:news_feed`.\n",
        "parameters": [
          {
            "name": "box",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "received",
                "sent"
              ],
              "default": "received"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "resolved",
                "all"
              ],
              "default": "open"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of feedback",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "feedback",
                    "meta"
                  ],
                  "properties": {
                    "feedback": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedPostFeedback"
                      }
                    },
                    "box": {
                      "type": "string",
                      "enum": [
                        "received",
                        "sent"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "resolved",
                        "all"
                      ]
                    },
                    "counts": {
                      "type": "object",
                      "description": "Rows in this box by status, before paging.",
                      "properties": {
                        "open": {
                          "type": "integer"
                        },
                        "resolved": {
                          "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"
                        }
                      }
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          }
        }
      }
    },
    "/news-feed/feedback/{id}": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "One piece of private feedback",
        "description": "Only its sender and the post's author may open it; to anyone else it\nis a 404. Opening it as the author marks it read. Asks `read:news_feed`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The feedback",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/NewsFeedPostFeedback"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "404": {
            "description": "`not_found` \"Feedback not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feedback/{id}/reply": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reply to feedback (the author, once) — resolves it",
        "description": "The author's one private reply; the sender is notified and the\nfeedback becomes resolved. Asks `write:news_feed`. 403 `not_recipient`\nfor the sender; 422 `already_replied`, or `invalid` for a blank reply.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reply_body"
                ],
                "properties": {
                  "reply_body": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/NewsFeedPostFeedback"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`not_recipient` — only the post's author can reply; or Communications is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feedback not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`already_replied` or `invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feedback/{id}/resolve": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark feedback resolved (the author)",
        "description": "Asks `write:news_feed`. 403 `not_recipient` for the sender; 422 `already_resolved`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/NewsFeedPostFeedback"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`not_recipient`, or Communications is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feedback not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`already_resolved`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feedback/{id}/reopen": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reopen resolved feedback (the author)",
        "description": "Asks `write:news_feed`. 403 `not_recipient` for the sender; 422\n`not_resolved`, or `newer_open` when the same person has newer open\nfeedback on the same post (reply to that one).\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reopened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedback": {
                      "$ref": "#/components/schemas/NewsFeedPostFeedback"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`not_recipient`, or Communications is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feedback not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_resolved` or `newer_open`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feedback/{id}/withdraw": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Withdraw your own feedback (the sender)",
        "description": "Asks `write:news_feed`. Deletes feedback the caller sent while it is\nstill open and unanswered; the author's Inbox ask about it closes.\n403 `not_sender` for the author; 422 `not_withdrawable` once it is\nanswered or resolved.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "withdrawn": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`not_sender`, or Communications is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feedback not found.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_withdrawable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/media": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Upload a News Feed attachment ahead of the post (orphan media)",
        "description": "Mobile/API composer flow. **Send the file itself** as a\n`multipart/form-data` `file` part (with `media_type` and, optionally,\n`alt_text` / `width_px` / `height_px`): the server reads the real size\nand content type off the bytes, stores the file, and returns a\nFeedMedia row that is already finalized, in \"orphan\" state (`feed_id`\nis null) — there is no second call. Files over 200 MB go up in parts\nthrough Upload Sessions (`POST /uploads/sessions`, purpose\n`news_feed`). Finally the post is created with `POST /feeds` and\n`feed_media_ids: [<id>, ...]` — which atomically claims the orphans and\nattaches them to the new feed.\n\nDEPRECATED leg, kept for clients already in the field and available\nonly on deployments that store files in S3: a JSON body with the\ndeclared metadata returns a presigned S3 PUT URL; the client PUTs the\nbytes and finalizes via `POST /media/{id}/complete`. On any other\nstorage provider it answers 422 `direct_upload_unsupported`.\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": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "media_type"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The attachment. Size and content type are read from the bytes, never from the client."
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "gif",
                      "video",
                      "file"
                    ]
                  },
                  "alt_text": {
                    "type": "string"
                  },
                  "width_px": {
                    "type": "integer",
                    "description": "Image/gif natural width — skips server-side Vips decode if supplied."
                  },
                  "height_px": {
                    "type": "integer"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Deprecated presigned-PUT leg (S3 deployments only).",
                "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. Multipart leg — the row is finalized and there is no `upload_url`. Deprecated JSON leg — PUT the 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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "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 transcode job — which produces a\nposter frame and, when the upload needs one, a 720p H.264 playback\nrendition (`playback_url`) — and (optionally) the auto-subtitle\npipeline. For files, flips status to `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"
          },
          "403": {
            "$ref": "#/components/responses/CommunicationsUnavailable"
          },
          "404": {
            "description": "Media not found (already claimed, deleted, or owned by another user)."
          }
        }
      }
    },
    "/media/gif": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Attach a GIF picked from KLIPY (orphan media)",
        "description": "The GIF picker's \"pick\": send the KLIPY `slug` a search returned\n(`GET /news-feed/gifs`). The server looks the GIF up on KLIPY,\ndownloads it and stores it exactly as an uploaded GIF — the same\norphan FeedMedia row `POST /media` creates, finalized, with the GIF's\nKLIPY title as its alt text — so it is claimed by `POST /feeds`\n(`feed_media_ids`) or a comment (`comment_media_ids`) like any other\nattachment, and never depends on KLIPY's CDN afterwards. The same\nrules as an upload apply first: the Media uploads and GIF uploads\nswitches, the 10-pending cap, the 100 MB GIF cap.\n\n`POST /feeds/{feed_id}/media/gif` is the same for a draft post (or,\nwith `parent_type: comment` + `parent_id`, a comment), behind that\ndoor's own edit rules.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "The KLIPY slug from GET /news-feed/gifs."
                  },
                  "q": {
                    "type": "string",
                    "description": "The search that found it (optional; reported to KLIPY with the share)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Orphan media row created and finalized.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "type": "object",
                      "description": "Serialized FeedMedia (media_type gif)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Composing isn't enabled for this account"
          },
          "422": {
            "description": "Refused. Upload codes as for `POST /media` (`media_uploads_disabled`,\n`gif_uploads_disabled`, `attachment_cap_reached`, `file_too_large`),\nor `gif_not_found` / `gif_invalid_slug` (no such GIF any more),\n`gif_too_large`, `gif_not_configured` (no KLIPY key on this\ndeployment), `gif_unavailable` (KLIPY did not answer).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{feed_id}/media/gif": {
      "parameters": [
        {
          "in": "path",
          "name": "feed_id",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Attach a GIF picked from KLIPY to a post or one of its comments",
        "description": "`POST /media/gif` for a post the caller may edit: the GIF is fetched by\nthe server and stored as the post's own media, exactly like an uploaded\nGIF, behind this post's edit rules (the post's author or a\nCommunications admin before it goes out; after, while content editing\nis on and nobody has acknowledged it — and the post is marked edited).\nWith `parent_type: comment` + `parent_id` it attaches to that comment\ninstead, by its author within the comment's edit window.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "The KLIPY slug from GET /news-feed/gifs."
                  },
                  "q": {
                    "type": "string",
                    "description": "The search that found it (optional)."
                  },
                  "parent_type": {
                    "type": "string",
                    "enum": [
                      "comment"
                    ]
                  },
                  "parent_id": {
                    "type": "integer",
                    "description": "The comment, when parent_type is comment."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Media row created and finalized.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/NewsFeedFeedMedia"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "The post can't be edited by the caller."
          },
          "404": {
            "description": "Post not found, or not visible to the caller."
          },
          "422": {
            "description": "Refused — the codes `POST /media/gif` answers, plus the comment\ncodes (`comment_not_found`, `not_comment_author`,\n`edit_window_expired`, `comment_attachment_cap_reached`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/gifs": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Search GIFs (KLIPY), or list trending ones",
        "description": "The GIF picker's search, proxied to KLIPY so no provider key reaches\nthe client. KLIPY's strictest content filter is always applied, and the\ncaller is identified to KLIPY only by an opaque per-person hash. A blank\n`q` returns what is trending. Requires the tenant's Media uploads and\nGIF uploads switches (Settings › Media & Voice). Pick one with\n`POST /media/gif`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Words to search for; blank for trending."
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of GIFs (24 per page).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "gifs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string",
                            "description": "What a pick sends back — KLIPY's per-response token included."
                          },
                          "key": {
                            "type": "string",
                            "description": "The GIF's durable name, the same on every page (the slug without its token) — tell a GIF already shown by this."
                          },
                          "title": {
                            "type": "string"
                          },
                          "preview_url": {
                            "type": "string",
                            "description": "A small preview on KLIPY's CDN, for the picker grid only."
                          },
                          "width": {
                            "type": "integer"
                          },
                          "height": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "has_next": {
                      "type": "boolean"
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "klipy"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`media_uploads_disabled` / `gif_uploads_disabled` — the tenant turned these off, or Communications is not available to the caller."
          },
          "502": {
            "description": "`gif_unavailable` — KLIPY did not answer."
          },
          "503": {
            "description": "`gif_not_configured` — this deployment has no KLIPY key."
          }
        }
      }
    },
    "/feeds/{feed_id}/media": {
      "parameters": [
        {
          "in": "path",
          "name": "feed_id",
          "required": true,
          "description": "The post (feed) id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List a post's attachments",
        "description": "The post's live attachments (images, GIFs, videos, files and link\npreviews), in gallery order (`position`). Files attached to the\npost's comments are listed too — they carry a `comment_id`; the\npost's own files have `comment_id: null`. The cover image is listed\nwith `cover: true`.\n\nReading the attachments is reading the post: the caller must be in\nits audience — or, before it goes out, one of the people preparing\nit — or hold the \"view any post\" right. A draft or scheduled post the\ncaller is not preparing answers `404` like a missing one.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "The post's attachments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "media"
                  ],
                  "properties": {
                    "media": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedFeedMedia"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NewsFeedMediaReadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NewsFeedFeedNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Attach a file to a post (or to one of its comments)",
        "description": "Adds an attachment to a post the caller may change. **Send the file\nitself** as a `multipart/form-data` `file` part with `media_type`\n(and, optionally, `alt_text`, `width_px`, `height_px`): the server\nreads the real size and type off the bytes, stores the file and\nanswers with the finished row — there is no second call. Files over\n200 MB go up in parts through Upload Sessions\n(`POST /uploads/sessions`, purpose `news_feed`).\n\n**Who may add to the post.** The same rule body edits use: before the\npost goes out, its author (or the person writing it for them) or a\nCommunications app admin whose reach covers it; after it went out,\nwhile the business's content-editing switch is on and until anyone\nhas acknowledged it. Anyone else gets `403 feed_not_editable`. A post\na delegate is writing for someone else is changed only by that person\nor the delegate. Adding to a published post marks it **edited**.\n\n**A comment's attachment.** With `parent_type: comment` and\n`parent_id`, the file attaches to that comment of this post instead;\nthe post's edit rule is skipped and the comment's own rules apply (its\nauthor, inside the comment edit window) — refusals come back as `422`\ncodes. Delete a comment's file with\n`DELETE /comments/{comment_id}/media/{id}`.\n\nDEPRECATED leg, kept for clients already in the field and available\nonly on deployments that store files in S3: a JSON body with the\ndeclared metadata reserves a row and returns a presigned PUT\n`upload_url`; PUT the bytes there, then call\n`PATCH /feeds/{feed_id}/media/{media_id}/complete`. On any other\nstorage it answers `422 direct_upload_unsupported`.\n\nLimits: 10 attachments per post (the cover image and link previews\ndo not count) and a per-comment cap for a comment's files; 100 MB per\nimage or GIF, 500 MB per video, 50 MB per file; blocked types and\nextensions; the admin's Media uploads and GIF uploads switches.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "media_type"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The attachment. Size and content type are read from the bytes, never from the client."
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "gif",
                      "video",
                      "file"
                    ]
                  },
                  "alt_text": {
                    "type": "string",
                    "description": "Required for images when the business requires alt text (`alt_text_required`)."
                  },
                  "width_px": {
                    "type": "integer",
                    "description": "Image/GIF natural width — skips the server-side decode when given."
                  },
                  "height_px": {
                    "type": "integer"
                  },
                  "parent_type": {
                    "type": "string",
                    "enum": [
                      "comment"
                    ],
                    "description": "Attach to a comment of this post instead of the post."
                  },
                  "parent_id": {
                    "type": "integer",
                    "description": "The comment, when parent_type is comment."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Deprecated presigned-PUT leg (S3 deployments only).",
                "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"
                  },
                  "height_px": {
                    "type": "integer"
                  },
                  "parent_type": {
                    "type": "string",
                    "enum": [
                      "comment"
                    ]
                  },
                  "parent_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Attachment created. Multipart leg — the row is finished and only `media` is returned. Deprecated JSON leg — also `upload_url`, `storage_key` and `expires_in`; PUT the bytes to `upload_url`, then call `complete`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "media"
                  ],
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/NewsFeedFeedMedia"
                    },
                    "upload_url": {
                      "type": "string",
                      "description": "Deprecated leg only. Presigned PUT URL."
                    },
                    "storage_key": {
                      "type": "string",
                      "description": "Deprecated leg only."
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Deprecated leg only. Seconds the upload_url stays valid."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NewsFeedMediaWriteForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NewsFeedFeedNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NewsFeedMediaRefused"
          }
        }
      }
    },
    "/feeds/{feed_id}/media/{media_id}": {
      "parameters": [
        {
          "in": "path",
          "name": "feed_id",
          "required": true,
          "description": "The post (feed) id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "in": "path",
          "name": "media_id",
          "required": true,
          "description": "The attachment id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one attachment of a post",
        "description": "One live attachment of the post — a post's own file or one of its\ncomments' files. Same read rule as the list.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "The attachment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "media"
                  ],
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/NewsFeedFeedMedia"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NewsFeedMediaReadForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NewsFeedMediaNotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remove an attachment from a post",
        "description": "Removes one of the post's own attachments, under the same rule as\nadding one (see `POST /feeds/{feed_id}/media`): anyone else gets\n`403 feed_not_editable`. A link preview stays removable by the post's\nauthor at any time. Removing from a published post marks it\n**edited**. The row is soft-deleted.\n\nA comment's attachment is not removed here — that answers\n`422 use_comment_media_endpoint`; use\n`DELETE /comments/{comment_id}/media/{id}`.\n\n**Required Scopes:** `destructive:news_feed` — removing a file is a\ndelete-level act. A token without it gets\n`403 insufficient_permissions` (\"Required scope:\ndestructive:news_feed\") before anything else is checked. The sign-in\ntokens hold it; a token made by hand holds it only when granted. A\nbrowser session is asked no scope.\n",
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:news_feed`.\n- `feed_not_editable` — the caller may not change this post's attachments. The message says why: \"You can't change the attachments on someone else's post.\", \"Attachments on this post can no longer be changed.\" (editing is off, or someone acknowledged it), or \"Only <name> and the person writing it for them can change this post's attachments before it goes out.\"\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NewsFeedMediaNotFound"
          },
          "422": {
            "description": "`use_comment_media_endpoint` — the file belongs to a comment; the message names the comment-scoped URL to call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{feed_id}/media/{media_id}/complete": {
      "parameters": [
        {
          "in": "path",
          "name": "feed_id",
          "required": true,
          "description": "The post (feed) id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "in": "path",
          "name": "media_id",
          "required": true,
          "description": "The attachment id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Finish a presigned upload (deprecated leg)",
        "description": "The deprecated JSON leg's second call, after the client PUT the bytes\nto the presigned `upload_url`: images and GIFs get their dimensions\nread (unless `width_px`/`height_px` were sent), videos are queued for\ntranscoding (a poster frame and, when needed, a 720p playback\nrendition) and files are marked ready. Finishing a post's file marks a\npublished post **edited**. The multipart leg never needs this call.\n\nOnly someone who could have made the upload may finish it: the person\nwho uploaded it, the comment's author for a comment's file, or someone\nwho may change the post's attachments now.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "The finished attachment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "media"
                  ],
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/NewsFeedFeedMedia"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed`.\n- `feed_not_editable` — a post's file the caller did not upload and may not change now (the message says why, as for DELETE).\n- `comment_not_editable` — \"Only the comment's author can change its attachments.\"\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NewsFeedMediaNotFound"
          }
        }
      }
    },
    "/news-feed/feeds/{id}/read_event": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record that the caller read a post",
        "description": "Logs the caller's view of a published post for its engagement\nanalytics. The first call records `read_at`; every call moves\n`last_seen_at`. A call reporting `scroll_depth_pct` of **80 or more**\nrecords a *qualified read* once (`qualified_read_at`), which also adds\na late reader to the post's audience snapshot and refreshes the post's\nanalytics. Idempotent — call it on every open.\n\nAn unacknowledged **must-read** logs no view: the call answers\n`200` with `logged: false` and `acknowledgement_required: true`, and\nnothing is written until the caller acknowledges the post\n(`POST /feeds/{id}/acknowledge`).\n\nThe caller must be able to view the post — its audience, or an\nadmin. A draft or scheduled post answers `404` to everyone except the\npeople preparing it, who get `422 not_published`.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "The post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scroll_depth_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "How far the caller scrolled, in percent. Clamped to 0..100. Omitted, zero, negative or not a number = a plain view (no qualified read).",
                    "example": 85
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's read record after this call. When `logged` is false (an unacknowledged must-read) every timestamp and the depth are null and `acknowledgement_required` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "logged",
                    "read_at",
                    "last_seen_at",
                    "qualified_read_at",
                    "scroll_depth_pct"
                  ],
                  "properties": {
                    "logged": {
                      "type": "boolean"
                    },
                    "acknowledgement_required": {
                      "type": "boolean",
                      "description": "Present (true) only when `logged` is false."
                    },
                    "read_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "First time the caller opened the post."
                    },
                    "last_seen_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "qualified_read_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "When the caller first scrolled 80% or more; null until then."
                    },
                    "scroll_depth_pct": {
                      "type": "number",
                      "nullable": true,
                      "description": "The depth recorded with the qualified read."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `forbidden` \"Cannot view this feed.\" — the caller is not in the post's audience.\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found.\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_published` — the post has not gone out yet (only its preparers get this).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/signals/{id}/complete": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "description": "The action-item signal id — a signal the post or one of its comments carries whose `signal_types` include `action_item`.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Mark an action item done",
        "description": "Marks an action item raised on a post (or one of its comments) done by\nthe caller. Who may: business admins, Communications app admins, the\nperson who wrote the post or comment that carries it, and the post's\nauthor — anyone else gets `403`. The caller must also be able to view\nthe post. Idempotent: a second call (or a double tap) answers the\nfirst completion's time.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signal_id",
                    "completed_at"
                  ],
                  "properties": {
                    "signal_id": {
                      "type": "integer"
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `forbidden` \"Feed not visible.\" — the caller may not view the post.\n- `forbidden` \"Not authorized to complete this action.\" — the caller is not an admin, a Communications app admin, or the author of the post or of the content that raised it.\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Signal not found.\" — no such signal in the business, or one on a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `not_published` — the post has not gone out yet (only its preparers get this).\n- `validation_failed` \"Not an action item.\" — the signal is not an action item.\n- `validation_failed` \"Could not mark this action done. Please try again.\" — the write failed; retry.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Undo the caller's own \"done\" on an action item",
        "description": "Removes the caller's own completion of the action item. Answers\n`204` whether or not the caller had marked it done; nobody else's\ncompletion is touched.\n\n**Required Scopes:** `write:news_feed` — undoing your own toggle is\nnot a delete-level act. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "responses": {
          "204": {
            "description": "The caller's completion is gone (or there was none)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Signal not found.\" — no such signal in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comments/{id}/reactions": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Comment id.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "List the people who reacted to a comment",
        "description": "The reactions on one comment in two shapes: `reactors`, a flat list\nordered by most recent reaction first (a swap counts as a new\nreaction), and `grouped`, the same reactors bucketed by `emoji_key`\nand sorted by count, largest first. At most the 200 most recent\nreactions are returned, and `total` counts the reactors returned\n(so it never exceeds 200) — the comment's full count is\n`aggregate.total` on the POST answer and `reactions.total` on the\ncomment itself.\n\nThe comment's post decides who may ask: a comment on a post outside\nthe caller's audience is `403 not_authorized`; a comment on a draft\nor scheduled post is `404 not_found` to everyone but the people\npreparing it, who get `422 not_published`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The comment's reactors, flat and grouped by emoji",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedReactorList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\" (the admin turned comments, reactions and replies off).\n- `not_authorized` — \"You do not have access to this comment\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Comment not found\" — no such comment in the business, a deleted comment, or one on a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_published` \"This post hasn't been published yet.\" — the comment is on a draft or scheduled post the caller is preparing; nobody engages with it before it goes out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "React to a comment (set, swap or toggle off)",
        "description": "Sets the caller's one reaction on the comment. With no reaction yet\nit is created; with a different emoji it is swapped in place; with\nthe SAME emoji it is removed (a toggle — `reaction` is then null).\nEvery branch answers 200 with the comment's refreshed `aggregate`,\npushes the new counts to everyone watching the post, and marks the\ncomment's post read for the caller.\n\n`emoji_key` must be one of the 14 catalog keys and — unless it is the\nreaction the caller already holds, which can always be toggled off —\none the business's reaction palette offers (an admin setting; by\ndefault the eight expressive keys). Anything else, including a\nmissing `emoji_key`, is `422 invalid_emoji`.\n\nThe comment's post decides who may react, as on the GET.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. The sign-in tokens and a token made\nby hand hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji_key"
                ],
                "properties": {
                  "emoji_key": {
                    "$ref": "#/components/schemas/NewsFeedEmojiKey"
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "emoji_key"
                ],
                "properties": {
                  "emoji_key": {
                    "$ref": "#/components/schemas/NewsFeedEmojiKey"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's reaction after the tap, and the comment's aggregate",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reaction",
                    "aggregate"
                  ],
                  "properties": {
                    "reaction": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/NewsFeedReactionRecord"
                        }
                      ],
                      "nullable": true,
                      "description": "The caller's reaction now; null when this tap removed it."
                    },
                    "aggregate": {
                      "$ref": "#/components/schemas/NewsFeedReactionsAggregate"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this comment\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Comment not found\" — no such comment in the business, a deleted comment, or one on a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `invalid_emoji` — \"That reaction isn't available.\" (not one of the 14 catalog keys, missing, or not in the business's reaction palette).\n- `not_published` — \"This post hasn't been published yet.\": the comment is on a draft or scheduled post the caller is preparing.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "News Feed"
        ],
        "summary": "Remove the caller's reaction from a comment",
        "description": "Removes the caller's reaction from the comment, whatever its emoji.\nAnswers `204` whether or not there was one. Unreacting is the\ncaller's own toggle, not a delete-level act. A removal sends the new\ncount to everyone viewing the comment, as adding one does.\n\nThe comment's post decides who may ask, as on the GET.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. The sign-in tokens and a token made\nby hand hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Removed, or there was nothing to remove."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this comment\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Comment not found\" — no such comment in the business, a deleted comment, or one on a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`not_published` \"This post hasn't been published yet.\" — the comment is on a draft or scheduled post the caller is preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comments/{comment_id}/media/{id}": {
      "parameters": [
        {
          "name": "comment_id",
          "in": "path",
          "required": true,
          "description": "Comment id.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Id of one of the comment's live attachments (a FeedMedia row).",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "News Feed"
        ],
        "summary": "Remove an attachment from a comment",
        "description": "Takes one file off a comment. This is the comment-scoped delete path;\na comment's files are uploaded through `POST /feeds/{feed_id}/media`\n(`parent_type=comment`) or `POST /media` + `comment_media_ids`, and\n`DELETE /feeds/{feed_id}/media/{media_id}` refuses them.\n\nThe removal is an edit of the comment, under the comment's edit\nrules: only its author, while the comment is live, within 15 minutes\nof when it was written, and while the business's \"Allow content\nediting\" setting is on. Anyone else — an admin included — gets\n`403 comment_not_editable`. Like any edit it stamps the comment\nedited (`edited_at`). The file is soft-deleted. While the business\nhas comments switched off, no comment is edited: `403\ncomments_disabled`.\n\nThe last file of a comment with no words is refused\n(`422 comment_would_be_empty`) rather than leaving a blank comment in\nthe thread.\n\nThe comment's post decides who may ask: a comment on a post outside\nthe caller's audience is `403 not_authorized`; on a draft or\nscheduled post it is `404 not_found` to everyone but the people\npreparing it, who get `422 not_published`.\n\n**Required Scopes:** `destructive:news_feed` — removing a file is a\ndelete-level act. A token without it gets\n`403 insufficient_permissions` (\"Required scope:\ndestructive:news_feed\") before anything else is checked. The sign-in\ntokens hold it; a token made by hand holds it only when granted. A\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:news_feed` (\"Required scope: destructive:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this comment\": the caller is not in the post's audience.\n- `comment_not_editable` — \"Only the comment's author can change its attachments.\" (the caller did not write it) or \"This comment's attachments can no longer be changed.\" (the author, after the 15-minute window, on a deleted comment, or with content editing turned off).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` \"Comment not found\" — no such comment in the business, or one on a draft or scheduled post the caller is not preparing.\n- `not_found` \"Media not found\" — no live attachment with this id on this comment.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `comment_would_be_empty` — \"Add some text or keep at least one attachment.\": this is the last file of a comment with no words.\n- `not_published` — \"This post hasn't been published yet.\": the comment is on a draft or scheduled post the caller is preparing.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/media/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Id of an orphan upload returned by `POST /media` or `POST /media/gif`.",
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "News Feed"
        ],
        "summary": "Discard an orphan upload",
        "description": "Discards a file the caller uploaded ahead of a post or comment\n(`POST /media`, `POST /media/gif`) and has not attached yet — the\ncomposer's \"remove\" on a pending attachment. The row is soft-deleted\nand can no longer be claimed through `feed_media_ids` or\n`comment_media_ids`.\n\nOnly the caller's own orphan rows in their business answer here. A\nrow someone else uploaded, one already claimed onto a post or\ncomment, or one already discarded is `404 not_found`, exactly like a\nmissing id; a claimed file is removed through\n`DELETE /feeds/{feed_id}/media/{media_id}` or\n`DELETE /comments/{comment_id}/media/{id}`.\n\n**Required Scopes:** `destructive:news_feed` — removing a file is a\ndelete-level act. A token without it gets\n`403 insufficient_permissions` (\"Required scope:\ndestructive:news_feed\") before anything else is checked. The sign-in\ntokens hold it; a token made by hand holds it only when granted. A\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Discarded."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:news_feed` (\"Required scope: destructive:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Media not found\" — no live, unattached upload with this id that the caller made in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/mentionable_users": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "People who can be @-mentioned on a post",
        "description": "The @-mention picker for a post's comment box and composer. Only\npeople the post's comments may mention are offered: the post's\naudience, less anyone the post is held from until a required\nlanguage is checked. At most 5 people are returned.\n\n- **`q` given** — active people in the business whose first, last,\n  preferred or full name, or email, contains `q` (case-insensitive;\n  the first 100 characters are used), names that START with it\n  first, then by first and last name. The caller is never offered.\n  Each row carries `meta`, a secondary line (job title and work\n  location as far as the People directory shows them to the caller,\n  then email), and `detail`: the person's job title, and — when two\n  people on the page share a name — the location, department or\n  Employee ID that tells them apart, each only as far as the People\n  directory shows it to the caller.\n- **`q` blank or absent** — the first 5 people of that audience by\n  first and last name, as `id` / `name` / `avatar_url`, plus the same\n  `detail`. The caller is not left out of this list.\n\nWho may ask: anyone who may read the post — its audience once it has\ngone out, the people preparing it before. A draft or scheduled post\nis `404 not_found` to anyone else.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post (feed) id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "What was typed after the \"@\". Blank lists the audience by name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 5 people the caller may mention on this post",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "users"
                  ],
                  "properties": {
                    "users": {
                      "type": "array",
                      "maxItems": 5,
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedMentionableUser"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `discussion_layer_disabled` — \"The discussion layer is disabled for this business.\"\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/poll_summary": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get the AI summary of a closed poll",
        "description": "The AI-written outcome of a poll post, for the summary card beside\nthe results. Read-only — there is no regenerate endpoint.\n\nAlways `200` for a poll the caller may read. When there is nothing\nto show, `summary` is null and `reason` says why, checked in this\norder:\n\n- `feature_disabled` — the business's AI poll summaries setting is off.\n- `not_closed` — the poll is still open.\n- `not_available` — no summary has been written yet, or the newest\n  attempt failed.\n\nOtherwise `summary` is the newest summary (always `succeeded`) and\nthere is no `reason`.\n\nWho may ask: anyone who may read the post — its audience once it has\ngone out, the people preparing it before — and, for a post that has\ngone out, holders of the \"view any post\" grant (by default the\nCommunications app admins) from outside its audience. A post that is\nnot a poll is `404 not_found`, as is a draft or scheduled poll to\nanyone not preparing it.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Poll post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The poll's summary, or null and the reason there is none",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "summary"
                  ],
                  "properties": {
                    "summary": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/NewsFeedPollSummary"
                        }
                      ],
                      "nullable": true
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "feature_disabled",
                        "not_closed",
                        "not_available"
                      ],
                      "description": "Present only when `summary` is null."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience and holds no \"view any post\" grant.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found\" — no such post in the business, a post that is not a poll, or a draft or scheduled poll the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/summary": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get the AI summary of a post's discussion",
        "description": "The newest AI summary of the post's comment thread (newest by\n`generated_at`). That row can be a failed attempt: `status` is then\n`failed`, `failure_reason` says why, and the `summary` fields are\nblank or defaults. A post with no summary yet is\n`404 not_found` \"No summary available\".\n\nWho may ask: anyone who may read the post — its audience once it has\ngone out (and, outside it, a holder of the \"view any post\" grant —\nCommunications app admins by default), the people preparing it\nbefore. A draft or scheduled post is `404 not_found` to anyone else.\nThe business's AI discussion summaries setting must be on\n(`403 forbidden` otherwise).\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. The sign-in tokens hold it; a\nbrowser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The newest discussion summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "summary"
                  ],
                  "properties": {
                    "summary": {
                      "$ref": "#/components/schemas/NewsFeedDiscussionSummary"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience and holds no \"view any post\" grant.\n- `forbidden` — \"Discussion summarization is disabled for this business\": the AI discussion summaries setting is off.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "- `not_found` \"Feed not found\" — no such post in the business, or a draft or scheduled post the caller is not preparing.\n- `not_found` \"No summary available\" — the post has no summary yet.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{id}/summary/regenerate": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Ask for a fresh AI summary of a post's discussion",
        "description": "Queues a new AI summary of the post's comment thread and answers\n`202` at once; the summary is written in the background and then\nread with `GET /feeds/{id}/summary`. No request body.\n\nOnly the post's author, a business admin (admin or above) or a\nCommunications app admin may ask (`403 not_authorized` otherwise),\nonly on a post that has gone out (`422 not_published` for a draft or\nscheduled post, even to the people preparing it), and only when the\npost has no successful summary yet or has gathered at least 20 new\ncomments since the last successful one (`422` otherwise). The\nbusiness's AI discussion summaries setting must be on. Each accepted\nrequest is recorded in the post's audit log (best effort — a failed\naudit write does not fail the request).\n\nEach request counts against the caller's hourly allowance for this\nfeature, shared with the web's Update Summary button (30 an hour).\nPast it the answer is `200` with `{ \"ok\": false, \"code\":\n\"rate_limited\", \"error\": \"…\", \"retry_at\": \"…\" }` and nothing is\nqueued; `retry_at` says when the allowance lifts (the start of the\nnext hour).\n\nThe post must be one the caller may read — its audience once it has\ngone out (or, outside it, a holder of the \"view any post\" grant), the\npeople preparing it before; a draft or scheduled post is\n`404 not_found` to anyone else.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. The sign-in tokens and a token made\nby hand hold it; a browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "The new summary is queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "previous_summary_id"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "enqueued"
                      ]
                    },
                    "previous_summary_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The newest successful summary at the time of the request; null when there was none."
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "The caller's hourly allowance for summaries is used up; nothing was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "code",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "rate_limited"
                      ]
                    },
                    "error": {
                      "type": "string",
                      "description": "\"You've reached the hourly limit for this feature. Try again after 3:00 PM.\""
                    },
                    "retry_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the hourly limit lifts (the start of the next UTC hour), the same moment the `error` sentence names."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `not_authorized` — \"You do not have access to this feed\" (the caller is not in the post's audience and holds no \"view any post\" grant) or \"Author or admin required\" (the caller is neither the post's author, a business admin nor a Communications app admin).\n- `forbidden` — \"Discussion summarization is disabled for this business\": the AI discussion summaries setting is off.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `not_published` \"This post hasn't been published yet.\" — the post is a draft or scheduled; nothing is summarized before it goes out.\n- `validation_failed` \"Update Summary is available after 20 new comments since the last summary.\" — the post has a successful summary and fewer than 20 live comments have been added since it was written.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feeds/{id}/analytics": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get a post's engagement analytics",
        "description": "The engagement figures for one post — the twin of the web Engagement\ndrawer: reach, reads, acknowledgements, reactions and action items,\nplus a day-by-day timeline of the cumulative read and acknowledgement\nrates.\n\nWho may: the post's author, a business admin, or a Communications app\nadmin. Anyone else gets `403 forbidden`. A draft or scheduled post\nanswers `404` to everyone except the people preparing it.\n\nThe figures are recomputed on every call (never served from the\n15-minute cache). Every count is taken over the post's audience\nsnapshot — the people it was sent to — so the author, and an admin\nreading a post aimed at others, are not counted. `metrics.reactions_count`\ncounts reactions on the post itself, not on its comments.\n\n`timeline` has one entry per day in the business's time zone, from the\nday the post was published (created, for an unsent post) to 30 days\nlater or today, whichever comes first; each entry's rates are\ncumulative to the end of that day.\n\nWorks council: while the post's audience holds a group whose names are\nhidden and that group is under the company's smallest group, reach,\nreads and acknowledgements (tiles and timeline) count only the people\nwho may be named, and `names_hidden` is added to the response.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The post's engagement figures",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedPostAnalytics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Not authorized to view post analytics.\" — the caller is not the post's author, a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found.\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/feeds/{id}/analytics/export": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Export a post's acknowledgement roster as CSV",
        "description": "The post's audience as a CSV file, one row per person in its audience\nsnapshot, ordered by last name then first name, with the time each\nperson acknowledged it (blank when they have not). The path also\nroutes with a `.csv` suffix; no other format is accepted.\n\nColumns: `feed_id`, `employee_id`, `employee_name`, `employee_email`,\n`acknowledged_at`. A person with no name shows their email, else\n\"(former employee)\". Name and email cells are guarded against\nspreadsheet formula injection.\n\nWorks council: people whose names are hidden get no row of their own;\nthey are counted in one closing row whose `employee_id` is blank and\nwhose `employee_name` is the note that says so.\n\n**Large audiences:** when the audience snapshot holds more than 1,000\npeople the file is not returned. The export is queued instead and the\ncall answers `202` with a JSON body whose `delivery` says where the\nfile will arrive: `email` — emailed to the caller when it is ready —\nor, for a caller no email can reach (no address on file), `in_app` —\na notification linking to the finished file under Background tasks\n(`/background_tasks/{task_id}`), which carries every row.\n\nWho may: the post's author, a business admin, or a Communications app\nadmin, as for `GET /news-feed/feeds/{id}/analytics`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post (feed) id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The CSV, streamed as an attachment named `engagement-<title>-<id>.csv` (the post's headline as a slug of at most 80 characters, or `post` when it has none).",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"engagement-<title>-<id>.csv\"`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "example": "feed_id,employee_id,employee_name,employee_email,acknowledged_at\n412,88,Ana Lopez,ana@example.com,2026-10-02 14:03:11 UTC\n412,91,Ben Ortiz,,\n"
                }
              }
            }
          },
          "202": {
            "description": "The audience is over 1,000 people; the export was queued and will be emailed, or sent to the caller's notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "delivery",
                    "message"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ]
                    },
                    "delivery": {
                      "type": "string",
                      "enum": [
                        "email",
                        "in_app"
                      ],
                      "description": "Where the file will arrive: `email`, or `in_app` (a notification linking to it) for a caller no email can reach."
                    },
                    "message": {
                      "type": "string",
                      "description": "The same, as a sentence to show the caller.",
                      "example": "Export queued (1450 rows). You'll get a notification with the download link when it's ready."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Not authorized to view post analytics.\" — the caller is not the post's author, a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found.\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/knowledge-base": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "List knowledge-base entries (admin)",
        "description": "Every verified answer in the business's knowledge base — the admin\ntable — newest first (`marked_at` descending), 50 per page. Each\nentry is a question post whose answer was marked correct.\n\nOptional filters: `status` (any other value is ignored, never a 400)\nand `q`, a full-text search of the question and answer that applies\nfrom 2 characters and then orders the page by relevance, newest\nfirst among equals.\n\nWho may: business admins and Communications app admins. Anyone else\ngets `403 forbidden`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Only entries in this state. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "outdated"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Full-text search; ignored when shorter than 2 characters.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (50 entries a page).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of entries, with the admin lifecycle fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "results",
                    "total",
                    "page",
                    "total_pages"
                  ],
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedKnowledgeBaseAdminEntry"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Entries matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Administrative access required.\" — the caller is not a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/knowledge-base/search": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Search the knowledge base",
        "description": "Searches the active verified answers the caller may see, ranked by\nrelevance (newest first among equals), 20 per page. An answer\ninherits the audience of the question post it came from: an answer\nfrom a post aimed at everyone is open to all, one from a post aimed at\ngroups only to their members, one from a post aimed at the author's\ndirect reports only to people who report to that author, and an answer\nfrom a question held from the caller's place until its required\nlanguage is checked is left out. Outdated answers are never returned.\n\nAny signed-in member may search; no admin role is needed.\n\nA query shorter than 2 characters (after trimming) returns no results\nand a smaller body — `{ \"results\": [], \"total\": 0, \"page\": 1 }`, with\nno `total_pages`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "The search text (full-text, English stemming).",
            "schema": {
              "type": "string"
            },
            "example": "parking permit"
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (20 results a page).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of matching answers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "results",
                    "total",
                    "page"
                  ],
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedKnowledgeBaseEntry"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "total_pages": {
                      "type": "integer",
                      "description": "Absent when the query was too short to search."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/knowledge-base/export": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Export the knowledge base (admin)",
        "description": "Every knowledge-base entry in the business, active and outdated,\nnewest first, as a CSV (the default) or JSON file. No filters apply.\nThe format is the `format` query parameter or a `.csv` / `.json`\nsuffix on the path; any other value falls back to CSV.\n\nCSV columns: `id`, `question_title`, `question_body`, `answer_text`,\n`answerer`, `segment`, `marked_at`, `status`, `outdated_reason`\n(text cells are guarded against spreadsheet formula injection). JSON\nis an array of `NewsFeedKnowledgeBaseExportRow`, which also carries\n`outdated_at`.\n\n**Large exports:** with more than 1,000 entries the file is not\nreturned. The export is queued and the call answers `202`, whose\n`delivery` says where the file will arrive: `email` — emailed to the\ncaller (cut to fit an email attachment, with the email saying how\nmany entries it holds) — or, for a caller no email can reach (no\naddress on file), `in_app` — a notification linking to the finished\nfile under Background tasks (`/background_tasks/{task_id}`), which\nholds every entry.\n\nWho may: business admins and Communications app admins. Anyone else\ngets `403 forbidden`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "description": "File format. Any other value means `csv`.",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "json"
              ],
              "default": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, as an attachment named `knowledge_base_<business_id>_<YYYYMMDD>.<csv|json>`.",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"knowledge_base_<business_id>_<YYYYMMDD>.<format>\"`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "example": "id,question_title,question_body,answer_text,answerer,segment,marked_at,status,outdated_reason\n31,Where do I park?,Where do I park during the works?,Lot B until June.,Ana Lopez,All employees,2026-09-14T10:02:00Z,active,\n"
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NewsFeedKnowledgeBaseExportRow"
                  }
                }
              }
            }
          },
          "202": {
            "description": "More than 1,000 entries; the export was queued and will be emailed, or sent to the caller's notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "async",
                    "delivery",
                    "message"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "async": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "delivery": {
                      "type": "string",
                      "enum": [
                        "email",
                        "in_app"
                      ],
                      "description": "Where the file will arrive: `email`, or `in_app` (a notification linking to it) for a caller no email can reach."
                    },
                    "message": {
                      "type": "string",
                      "description": "The same, as a sentence to show the caller.",
                      "example": "Export is being generated. You'll receive an email when it's ready."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Administrative access required.\" — the caller is not a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/knowledge-base/{id}/mark_outdated": {
      "patch": {
        "tags": [
          "News Feed"
        ],
        "summary": "Mark a verified answer outdated (admin)",
        "description": "Retires a verified answer: its status becomes `outdated`, it drops out\nof knowledge-base search and Auto-Answer stops serving it, and an\naudit row is written. The person who gave the answer is notified,\nunless they are the one marking it. Only the web can mark it current\nagain.\n\nIdempotent: an entry that is already outdated is returned unchanged\n(its first reason is kept) and nobody is notified again.\n\nWho may: business admins and Communications app admins. Anyone else\ngets `403 forbidden` — checked before the entry is looked up.\n\n**Required Scopes:** `destructive:news_feed` — retiring an answer is\na delete-level act. A token without it gets\n`403 insufficient_permissions` (\"Required scope: destructive:news_feed\")\nbefore anything else is checked; a token a person makes by hand does\nnot hold it. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge-base entry id.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Why the answer is outdated. Trimmed and cut to 500 characters; blank stores no reason.",
                    "example": "The parking policy changed on October 1."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entry after the call",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "entry"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "entry": {
                      "$ref": "#/components/schemas/NewsFeedKnowledgeBaseAdminEntry"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `destructive:news_feed` (\"Required scope: destructive:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Administrative access required.\" — the caller is not a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"The requested resource was not found.\" — no such entry in the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` \"Couldn't update the entry: …\" — the stored entry no longer passes its own validations, so it was left as it was.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/smes": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "List the experts on a topic",
        "description": "The subject-matter experts for one topic: the people an admin\nassigned to it on the SMEs page first (oldest assignment first,\n`designation: assigned`), then the people the weekly scoring ranks\nas experts (highest score first, `designation: measured`), without\nrepeats, up to `limit`. People who opted out of the topic are never\nlisted, and nor is anyone no longer a member of the business. A topic\nid with no experts (or not in the business) returns an empty list.\n\nWho may: decided by the Communications setting `sme_tab_visibility` —\n`all_employees` (the default; anyone), `managers_and_above` (managers,\nbusiness admins and Communications app admins) or `admins_only`\n(business admins and Communications app admins). Anyone else gets\n`403 forbidden`, checked before `topic_id`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "topic_id",
            "in": "query",
            "required": true,
            "description": "The topic id. Missing, zero, non-numeric or not a single value is a `400`.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Most experts to return. Missing or 0 means 5; otherwise clamped to 1..50.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The topic's experts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedSmeExpert"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` \"topic_id is required.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"The experts directory is restricted for this workspace.\" — `sme_tab_visibility` does not include the caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/smes/by_topic": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "List the experts on several topics",
        "description": "The same answer as `GET /news-feed/smes` for up to 50 topics at once,\nat most 5 experts per topic. The `data` object is keyed by topic id\n(as a string), in the order the ids were sent; a topic with no\nexperts maps to an empty array. Duplicate, zero and non-numeric ids\nare dropped, and ids past the 50th are ignored. No ids → `{ \"data\": {} }`.\n\nWho may: the `sme_tab_visibility` rule described on `GET /news-feed/smes`.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "topic_ids[]",
            "in": "query",
            "description": "Topic ids, repeated (`topic_ids[]=3&topic_ids[]=7`). At most 50 are read.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "maxItems": 50,
              "items": {
                "type": "integer"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Experts per topic",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "Topic id (string) → that topic's experts.",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/NewsFeedSmeExpert"
                        }
                      },
                      "example": {
                        "3": [
                          {
                            "id": 88,
                            "name": "Ana Lopez",
                            "title": "Facilities Lead",
                            "topic_id": 3,
                            "designation": "assigned"
                          }
                        ],
                        "7": []
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"The experts directory is restricted for this workspace.\" — `sme_tab_visibility` does not include the caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/smes/gaps": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "List knowledge gaps (admin)",
        "description": "Every active topic people posted about in the last 30 days, with\nhow many experts it has, computed at the time of the request — the\nsame list the Experts page's \"Topics that need experts\" is drawn\nfrom, so the two always agree. `high` severity first, then `medium`,\nthen `low`; within each, fewest experts, then most posts, then name.\nNot paginated.\n\nSeverity: `high` — no expert and 5 or more posts; `medium` — no\nexpert and 1–4 posts, or one expert and 5 or more posts; `low` —\neverything else. `needs_experts` is true for `high` and `medium`,\nthe rows the Experts page lists. Experts counts both the people an\nadmin added on the Experts page and those found automatically.\n\nWho may: business admins and Communications app admins. Anyone else\ngets `403 forbidden`. (The `sme_tab_visibility` setting does not\napply here.)\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The business's knowledge gaps",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsFeedKnowledgeGap"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `forbidden` \"Admin access required.\" — the caller is not a business admin or a Communications app admin.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/user-preferences/cc": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get the caller's closed-captions preference",
        "description": "The caller's closed-captions preference for feed videos and what it\nresolves to. `auto` resolves against the caller's language — their\nprofile locale, else the first tag of the `Accept-Language` header:\na language other than English turns captions on; English, or no\nlanguage at all, leaves them off.\n\nThe preference row is created (as `auto`) on the first read, so this\nGET can write.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's preference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedCcPreference"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "summary": "Set the caller's closed-captions preference",
        "description": "Sets the caller's closed-captions preference and answers it with its\nresolved value (see GET).\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cc_preference"
                ],
                "properties": {
                  "cc_preference": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "on",
                      "off"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's preference after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedCcPreference"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_value` \"cc_preference must be one of auto, on, off\" — `cc_preference` is missing or not one of the three values. The sentence is also repeated in a top-level `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/user-preferences/read-aloud": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get the caller's Read Aloud preferences",
        "description": "The caller's own Read Aloud voice and speed. Each is `null` when the\ncaller has not chosen one — the player then uses the admin's default\nvoice (else `neutral`) and normal speed (1.0).\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's Read Aloud preferences",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedReadAloudPreference"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "News Feed"
        ],
        "summary": "Set the caller's Read Aloud preferences",
        "description": "Changes the keys sent and leaves the others alone. Sending a key as\n`null` (or an empty string) clears the caller's choice, falling back\nto the default. Nothing is saved when either value is invalid.\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tts_voice": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "neutral",
                      "male",
                      "female",
                      null
                    ]
                  },
                  "read_aloud_speed": {
                    "type": "number",
                    "nullable": true,
                    "enum": [
                      0.75,
                      1.0,
                      1.25,
                      1.5,
                      2.0,
                      null
                    ],
                    "description": "A number or numeric string. A list (`read_aloud_speed[]=…`) is refused with `422`."
                  }
                }
              },
              "example": {
                "tts_voice": "female",
                "read_aloud_speed": 1.25
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's Read Aloud preferences after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedReadAloudPreference"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A value was refused. The body is NOT the error envelope: it is `{ \"errors\": { \"<field>\": [\"<message>\"] } }` — `tts_voice` \"must be one of neutral, male, female or null\", `read_aloud_speed` \"must be one of 0.75, 1.0, 1.25, 1.5, 2.0\", or a stored-row validation message under the model's field name.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "errors"
                  ],
                  "properties": {
                    "errors": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "example": {
                    "errors": {
                      "read_aloud_speed": [
                        "must be one of 0.75, 1.0, 1.25, 1.5, 2.0"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/voice/transcribe": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Upload a voice recording for transcription",
        "description": "Whisper-mode voice-to-text for the composer and comments. Uploads the\nrecording, creates a pending transcript and queues the transcription;\nthe answer is `202` with the transcript's id. The result arrives on\nthe `VoiceTranscriptChannel` broadcast, or poll\n`GET /news-feed/voice/{id}`. The audio is deleted once transcribed.\n\nTranscription draws the business's AI credits. A business whose AI\nusage is capped, a recording with no speech in it, or a transcription\nthat fails three times ends with the transcript `failed` and a\n`failure_reason` — the upload itself still answers `202`.\n\nNeeds the Communications setting `voice_sharing` on (else `422\nvoice_sharing_disabled`) and `voice_transcription_mode` set to\n`whisper` (else `422 whisper_mode_disabled`; in `native` mode the\ndevice transcribes and never calls this).\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "audio"
                ],
                "properties": {
                  "audio": {
                    "type": "string",
                    "format": "binary",
                    "description": "The recording, at most 25 MB, of type `audio/webm`, `audio/mpeg`, `audio/mp4`, `audio/m4a`, `audio/x-m4a`, `audio/wav` or `audio/ogg` (the part's declared content type)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The transcript was created and its transcription queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedVoiceTranscript"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `voice_sharing_disabled` — voice sharing is off for the business.\n- `whisper_mode_disabled` — the business transcribes on the device (`native` mode).\n- `missing_file` \"Audio file is required\".\n- `file_too_large` \"File exceeds 25 MB\".\n- `unsupported_mime` \"Unsupported audio type: <type>\".\n\nThe upload refusals also repeat the sentence in a top-level `message`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_error` \"Failed to upload audio\" — the recording could not be stored; try again. No transcript is kept for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/voice/{id}": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get a voice transcript",
        "description": "One of the caller's own voice transcripts — the polling fallback for\na client that missed the broadcast. Only the person who uploaded the\nrecording may read it; anyone else's id, like a missing one, is `404`.\n\nNeeds the Communications setting `voice_sharing` on (else `422\nvoice_sharing_disabled`). Unlike the upload, it does not need\n`whisper` mode.\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The transcript id (digits only; anything else does not route).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedVoiceTranscript"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no transcript with this id that the caller uploaded in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`voice_sharing_disabled` — voice sharing is off for the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/tts/generate": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get Read Aloud audio for a post",
        "description": "A playable audio file of a post's body, read aloud. The audio for a\npost, voice and language is generated once and cached for 24 hours;\na cache hit (`cached: true`) charges nobody, a miss draws the\ncaller's business's AI credits. Editing the post's text discards its\ncached audio.\n\n`audio_url` is a signed link valid for **1 hour**; `expires_at` is\nwhen that link stops working (the hour, or sooner when the cached\naudio expires first), so call again for a fresh link by then.\n\nThe caller must be able to read the post: its audience (or its\nauthor) once it has gone out; the people preparing it while it is a\ndraft or scheduled. Any other post — or no `feed_id` — is `404`, so\nthe answer never says a post exists.\n\n`voice` defaults to the caller's own Read Aloud choice, else the\nadmin's default, else `neutral`.\n\n**What it speaks is the language the reader is shown the post in**,\ndecided by the server with the feed's own rule: the reader's language\nwhen the company offers it and its translation of this post is ready;\notherwise the original (also for the reader's own post, a draft, or a\npost they chose \"Show original\" on). The response's `language` says\nwhich, and `title` is the headline in that language. A `language` the\ncaller sends is honoured only when it is `source` (the original) or one\nof the company's offered languages; any other value is ignored.\n\nNeeds the Communications setting `read_aloud` on (else `422\nread_aloud_disabled`).\n\n**Required Scopes:** `write:news_feed` (it is a POST). A token without\nit gets `403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "feed_id"
                ],
                "properties": {
                  "feed_id": {
                    "type": "integer",
                    "description": "The post (feed) id."
                  },
                  "voice": {
                    "type": "string",
                    "enum": [
                      "neutral",
                      "male",
                      "female"
                    ]
                  },
                  "language": {
                    "type": "string",
                    "description": "Optional. `source` for the original, or one of the company's offered language codes (as the post page's language picker); anything else is ignored and the reader's own rule decides.",
                    "example": "es"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The audio for the post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedTtsAudio"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no such post in the business, or one the caller may not read (including a draft or scheduled post they are not preparing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `read_aloud_disabled` — Read Aloud is off for the business.\n- `invalid_voice` \"voice must be one of neutral, male, female\".\n- `no_readable_content` \"Feed has no readable text\" — the post has no body text to read.\n- `ai_usage_blocked` — the business's (or the caller's) AI credits are used up; the message is the policy's sentence.\n\nThe service refusals also repeat the sentence in a top-level `message`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`tts_unavailable` — speech generation failed, or the audio was generated but no playable link could be signed. Either way the `message` reads \"Read Aloud isn't available right now. Try again in a moment.\" Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/ai/enhance": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Rewrite a draft post or comment with AI",
        "description": "Rewrites a draft for grammar, clarity and tone — the composer's\n\"Enhance with AI\" button. Nothing is saved; the rewritten text is\nreturned for the author to accept or reject. The call draws the\nbusiness's AI credits.\n\nThe input limit is the server's, never the client's: 1,000\ncharacters for a comment (`content_type: comment`), 5,000 for\nanything else; the rewrite is cut to the same limit. The draft must\nhold at least 20 non-blank characters.\n\nEach person may call it 120 times an hour. Past that the call\nanswers **`200`** with `{ \"ok\": false, \"error\": \"<sentence>\",\n\"code\": \"rate_limited\", \"retry_at\": \"<when it lifts>\" }` and no\n`enhanced_text`.\n\nNeeds the Communications setting `ai_enhanced_authoring` on (else\n`403 feature_disabled`, checked before the rate limit and the body).\n\n**Required Scopes:** `write:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "The draft. Missing or blank is a `400`."
                  },
                  "tone": {
                    "type": "string",
                    "enum": [
                      "professional",
                      "friendly",
                      "urgent",
                      "concise"
                    ],
                    "default": "professional",
                    "description": "Any other value means `professional`."
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "update",
                      "question",
                      "poll",
                      "comment"
                    ],
                    "default": "update",
                    "description": "What is being written. Any other value means `update`."
                  },
                  "instruction": {
                    "type": "string",
                    "enum": [
                      "fix_grammar",
                      "shorter",
                      "longer"
                    ],
                    "description": "A narrower rewrite: `fix_grammar` (correctness only), `shorter` or `longer`. Omitted or unknown means the default enhance."
                  },
                  "writing_profile_id": {
                    "type": "integer",
                    "description": "A ready writing profile of this business whose trained voice the rewrite should follow. An id that is not one is ignored."
                  }
                }
              },
              "example": {
                "text": "hey team the parking lot B will be closed tmrw for repaving so pls use lot C",
                "tone": "friendly",
                "content_type": "update"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rewritten text — or, past the hourly limit, the rate-limit body (`ok: false`, `code: rate_limited`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "enhanced_text"
                      ],
                      "properties": {
                        "enhanced_text": {
                          "type": "string"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/NewsFeedAiRateLimited"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — `text` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n- `feature_disabled` — AI-enhanced authoring is off for the business.\n- `ai_blocked` — the business's (or the caller's) AI usage policy refused the call; the message is the policy's sentence, also repeated in a top-level `message`. Not transient: do not retry.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "- `empty_text` — fewer than 20 non-blank characters.\n- `over_input_limit` — longer than 1,000 characters (comment) or 5,000 (anything else).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`service_error` — the AI returned nothing; try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/ai/suggest_topics": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Suggest topics for a draft post",
        "description": "Up to 3 topic suggestions for the composer's \"AI suggests #x #y\"\nhint. Nothing is saved. Suggestions are topic slugs; when the\nCommunications setting that allows open topics is off, only topics in\nthe business's active taxonomy are offered.\n\nNever an error for the author: the call answers `200` with an empty\nlist and `source: disabled` when the Communications settings\n`ai_auto_topic` or `author_topic_suggestions_in_composer` is off, or\nwhen the draft holds fewer than 50 non-blank characters; and with\n`source: error` when the AI call fails or times out.\n\n**Required Scopes:** `write:news_feed` (it is a POST). A token without\nit gets `403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "The draft. Missing or blank is a `400` — but only once both topic settings are on."
                  },
                  "segment_id": {
                    "type": "string",
                    "description": "The audience the draft is aimed at, as context for the AI: `direct_reports`, or a recipient-group id of this business. Anything else is ignored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The suggestions (possibly none)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "suggestions",
                    "source"
                  ],
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "maxItems": 3,
                      "items": {
                        "type": "string",
                        "description": "A topic slug.",
                        "example": "parking"
                      }
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "taxonomy",
                        "open",
                        "mixed",
                        "empty",
                        "disabled",
                        "error"
                      ],
                      "description": "- `taxonomy` / `open` / `mixed` — the suggestions came from the business's topics, new topics, or both.\n- `empty` — the AI found nothing confident.\n- `disabled` — a topic setting is off, or the draft is under 50 characters.\n- `error` — the AI call failed or timed out.\n"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — `text` is missing or blank (only checked once the feature is on).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/events": {
      "post": {
        "tags": [
          "News Feed"
        ],
        "summary": "Record a client analytics event",
        "description": "A fire-and-forget beacon for events only the client sees (a video\nthat froze, a reply preview tapped, a Read Aloud play, an AI enhance\naccepted …). Answers `204` with no body. Only the event names listed\nare accepted; events the server can see for itself are recorded\nserver-side and refused here.\n\n`feed_id` is optional. When sent, the caller must be able to read the\npost (else `404`); an event about a draft or scheduled post is\ndropped with a `204` and not recorded.\n\nEvent properties are read from the body by event: voice events\n(`voice_*`) take `surface`, `mode`, `duration_ms_to_transcript`,\n`char_count`, `chars_changed`; Read Aloud events (`tts_play_*`) take\n`voice`, `language`, `speed`, `fraction_played`, `reason`; enhance\nevents (`enhance_*`, `accept_all`, `reject_all`) take `content_type`,\n`tone`, `duration_ms`; `reply_preview_tapped` takes\n`new_replies_count`, `seconds_since_last_seen`;\n`news_feed.poll.voted` takes `option_count`, `voting_mode`,\n`is_change`; `news_feed.poll.chart_render_error` takes\n`error_message`; `video_cc_toggled` takes `feed_media_id`, `enabled`;\n`media_load_error` and `mobile_video_freeze` take `feed_media_id`,\n`error_code`, `network_type`. Strings are cut short (16–200\ncharacters), anything else is ignored, and the caller's user agent is\nrecorded with every event.\n\n**Required Scopes:** `write:news_feed` (it is a POST). A token without\nit gets `403 insufficient_permissions` (\"Required scope: write:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event"
                ],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": [
                      "media_load_error",
                      "mobile_video_freeze",
                      "reply_preview_tapped",
                      "news_feed.poll.voted",
                      "news_feed.poll.chart_updated_live",
                      "news_feed.poll.chart_render_error",
                      "news_feed.poll.presentation_started",
                      "enhance_clicked",
                      "enhance_returned",
                      "accept_all",
                      "reject_all",
                      "enhance_timeout",
                      "voice_record_started",
                      "voice_transcribed",
                      "voice_transcript_edited",
                      "voice_post_published",
                      "tts_play_started",
                      "tts_play_completed",
                      "tts_play_paused",
                      "video_cc_toggled"
                    ]
                  },
                  "feed_id": {
                    "type": "integer",
                    "description": "The post the event is about, if any."
                  }
                },
                "additionalProperties": true
              },
              "example": {
                "event": "tts_play_completed",
                "feed_id": 412,
                "voice": "neutral",
                "fraction_played": 0.92
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Recorded (or, for a draft or scheduled post, dropped)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `write:news_feed` (\"Required scope: write:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` \"Feed not found.\" — a `feed_id` was sent for a post that is not in the business or that the caller may not read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` \"unknown event\" — `event` is missing or not one of the accepted names.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/news-feed/media/{media_id}/subtitles": {
      "get": {
        "tags": [
          "News Feed"
        ],
        "summary": "Get a video's subtitle status",
        "description": "Whether a feed video has auto-generated subtitles, and where to load\nthem — the player asks this when a viewer opens a video, to decide\nwhether to add a captions track. The answer is carried by the HTTP\nstatus and by `status` in the body (never the error envelope):\n\n- `200` `ready` — with `webvtt_url`, a WebVTT file (a signed link\n  valid for an hour, or a CDN link).\n- `202` `pending` — subtitles are being generated; ask again later.\n- `422` `failed` — generation failed.\n- `404` `not_applicable` — this file gets no subtitles.\n- `404` `not_found` — no such live file in the business, or one on a\n  post the caller may not read (the audience of a sent post; the\n  people preparing a draft or scheduled one).\n\n**Required Scopes:** `read:news_feed`. A token without it gets\n`403 insufficient_permissions` (\"Required scope: read:news_feed\")\nbefore anything else is checked. A browser session is asked no scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "media_id",
            "in": "path",
            "required": true,
            "description": "The post attachment (feed media) id (digits only; anything else does not route).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subtitles are ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "webvtt_url"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ready"
                      ]
                    },
                    "webvtt_url": {
                      "type": "string",
                      "format": "uri",
                      "nullable": true,
                      "description": "The WebVTT file; null if no link could be made for it."
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Subtitles are being generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedSubtitleStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- `insufficient_permissions` — the token lacks `read:news_feed` (\"Required scope: read:news_feed\").\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`{ \"status\": \"not_applicable\" }` or `{ \"status\": \"not_found\" }` — see above.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedSubtitleStatus"
                }
              }
            }
          },
          "422": {
            "description": "`{ \"status\": \"failed\" }` — subtitle generation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsFeedSubtitleStatus"
                }
              }
            }
          }
        }
      }
    },
    "/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\n**Media may come in on this request as multipart file parts.** Each\n`media_uploads[]` entry carries its bytes as a `file` part instead of a\n`blob_signed_id` — one request, no pre-uploaded blob and nothing to\nclean up (see `POST /inspections/direct_uploads`, deprecated). Send the\nbody as `multipart/form-data`, with the entries in order:\n`media_uploads[][item_id]` (or `[template_item_id]`),\n`media_uploads[][file]`, optional `media_uploads[][kind]`. A\nJSON-only call is unchanged.\n\nRefused files (unsupported type — checked against the real bytes, not\nthe declared type — or over the per-type cap) fail the whole call with\n422 and nothing is written or stored.\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"
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/InspectionCreateRequest"
                  },
                  {
                    "type": "object",
                    "description": "Same fields, form-encoded, plus the file parts: `media_uploads[][file]` where a JSON body would carry `media_uploads[][blob_signed_id]`."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Inspection created (and optionally submitted)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "corrective_action_handoff": {
                      "$ref": "#/components/schemas/InspectionCorrectiveActionHandoff"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Template not found or inactive"
          },
          "422": {
            "description": "Validation failed; `template_has_no_items` — the template has no item to ask yet, so no inspection starts from it; `upload_blocked` — the virus scan found malware in a `media_uploads[][blob_signed_id]` file (that upload is deleted and nothing is written); `upload_incomplete` — a referenced upload does not match its declared checksum. With `complete: true`, also the completion gates of the complete endpoint (`items_pending`, `note_required`, `failure_details_required`, …)"
          },
          "503": {
            "description": "`scanner_unavailable` — uploads are paused while virus scanning is unavailable, or `storage_unavailable` — a referenced upload could not be read. Nothing was written; retry the call"
          }
        }
      }
    },
    "/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`.) Each item's `photo_required` is true when it must\ncarry a photo before the inspection can be submitted — an item whose\ntemplate sets `requires_photo`, or a required photo-answer item — the\nrule the submit enforces.\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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\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\nA `corrective_actions` entry's `status` follows the rules of\n`PATCH /inspections/corrective_actions/{id}`: `cancelled` is never\nwritten, and a completed or cancelled action's status does not change\n(`completed` sent again for a completed action is a no-op). Such an entry is\nskipped whole and the rest of the request still saves (owner decision\n2026-10-05, #6883); the response lists it in\n`skipped_corrective_actions`. `completed` stamps the action's completion\ndate, as the dedicated endpoint does. Where the company requires a photo\nto complete a corrective action, `completed` on an action with no photo\nis skipped the same way (`reason: completion_photo_required`), and a new\naction sent as `completed` is added open (`in_progress` with an assignee, else\n`pending`) and listed there too;\ncomplete it with its photo through\n`POST /inspections/corrective_actions/{id}/complete`.\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"
                    },
                    "skipped_corrective_actions": {
                      "$ref": "#/components/schemas/InspectionSkippedCorrectiveActions"
                    },
                    "corrective_action_handoff": {
                      "$ref": "#/components/schemas/InspectionCorrectiveActionHandoff"
                    }
                  }
                }
              }
            }
          },
          "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` and `items` (`[{id, name}]`,\nthe items still owed). Also\n`note_required` — an answered item the template asks a note for carries\nnone (details `items: [{id, name}]`); `failure_details_required` — a\nfailed item on a template that requires failure details still lacks\none (details `items: [{id, name, missing: [...]}]`, `missing` naming\nwhat each lacks: `severity`, `comment`, `photo`, `action_item`).\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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\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\nA `corrective_actions` entry's `status` follows the rules of\n`PATCH /inspections/corrective_actions/{id}`: `cancelled` is never\nwritten, and a completed or cancelled action's status does not change\n(`completed` sent again for a completed action is a no-op). Such an entry is\nskipped whole and the rest of the sync still saves (owner decision\n2026-10-05, #6883); the response lists it in\n`skipped_corrective_actions`, and an Idempotency-Key replay returns the\nsame list. `completed` stamps the action's completion date, as the\ndedicated endpoint does. Where the company requires a photo to complete\na corrective action, `completed` on an action with no photo is skipped\nthe same way (`reason: completion_photo_required`), and a new action\nsent as `completed` is added open (`in_progress` with an assignee, else\n`pending`) and listed there too.\n\n**Media may come in on this request as multipart file parts.** Each\n`media_uploads[]` entry carries its bytes as a `file` part instead of a\n`blob_signed_id` — one request, no pre-uploaded blob and nothing to\nclean up (see `POST /inspections/direct_uploads`, deprecated). Send the\nbody as `multipart/form-data`, with the entries in order:\n`media_uploads[][item_id]` (or `[template_item_id]`),\n`media_uploads[][file]`, optional `media_uploads[][kind]`. A\nJSON-only call is unchanged.\n\nRefused files (unsupported type — checked against the real bytes, not\nthe declared type — or over the per-type cap) fail the whole call with\n422 and nothing is written or stored.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionSyncRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/InspectionSyncRequest"
                  },
                  {
                    "type": "object",
                    "description": "Same fields, form-encoded, plus the file parts: `media_uploads[][file]` where a JSON body would carry `media_uploads[][blob_signed_id]`."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    },
                    "skipped_corrective_actions": {
                      "$ref": "#/components/schemas/InspectionSkippedCorrectiveActions"
                    },
                    "corrective_action_handoff": {
                      "$ref": "#/components/schemas/InspectionCorrectiveActionHandoff"
                    },
                    "conflicts": {
                      "type": "array",
                      "description": "Present only when the request carried a `base_version`. Each record whose server row changed after the client's base; its fields were NOT applied.",
                      "items": {
                        "type": "object",
                        "required": [
                          "type",
                          "id",
                          "server_version",
                          "message"
                        ],
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "inspection",
                              "item"
                            ]
                          },
                          "id": {
                            "type": "integer"
                          },
                          "server_version": {
                            "type": "string",
                            "description": "The record's token as of this response (the same value as its `server_version` in `inspection`) — rebase on it."
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "completion_deferred": {
                      "type": "boolean",
                      "description": "True when `complete: true` was sent but conflicts prevented submission. The response is cached under its Idempotency-Key, so the rebased retry needs a new key."
                    }
                  }
                }
              }
            }
          },
          "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`, `note_required` or `failure_details_required` —\nsee the complete endpoint; conditionally-hidden and\nnot-currently-required items don't block. Also\n`invalid_base_version` when any `base_version` in the body is not\nan ISO-8601 string (checked before anything is written or stored).\n`upload_blocked` when the virus scan finds malware in a\n`media_uploads[][blob_signed_id]` file (that upload is deleted and\nnothing in the batch is written); `upload_incomplete` when such an\nupload does not match its declared checksum (upload it again).\n"
          },
          "503": {
            "description": "`scanner_unavailable` — uploads are paused while virus scanning is unavailable, or `storage_unavailable` — a referenced upload could not be read. Nothing was written; retry the call"
          }
        }
      }
    },
    "/inspections/inspections/{id}/take_over": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Take over (or pick up) an inspection",
        "description": "Reassigns an in-progress inspection to the current caller (takeover),\nor a scheduled one that hasn't started when the tenant allows pickup.\nAll captured answers/photos/notes are preserved and the displaced\ninspector is notified. Eligibility (`Inspection#takeable_by?`): a\nmanager/admin or a peer assigned to the inspection's location who is\nnot already the inspector, and who can view the inspection. The\n`can_take_over` flag on the inspection payload signals eligibility.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionTakeOverRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection reassigned to the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "inspection": {
                      "$ref": "#/components/schemas/InspectionDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — caller cannot view or is not eligible to take over this inspection"
          },
          "422": {
            "description": "Inspection can no longer be taken over (wrong state)"
          }
        }
      }
    },
    "/inspections/inspections/{id}/report_pdf": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "include_photos",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "default": "true"
          },
          "description": "Pass `false` to omit photo evidence from the PDF."
        },
        {
          "name": "include_corrective_actions",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "default": "true"
          }
        },
        {
          "name": "include_audit_trail",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "default": "true"
          },
          "description": "Pass `false` to omit the GPS / signature / timestamp page."
        },
        {
          "name": "include_ai_summary",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "default": "true"
          },
          "description": "Pass `false` to omit the AI-generated executive summary."
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Download the inspection report as a PDF",
        "description": "Streams the inspection report PDF (mirrors the desktop \"Export PDF\"\nbutton). Sections are toggleable via the `include_*` query params —\nphotos, corrective actions, audit trail, and AI summary all default\non. Returns `application/pdf` with a `Content-Disposition: attachment`\nfilename of `inspection-report-<id>-<YYYYMMDD>.pdf`.\n",
        "responses": {
          "200": {
            "description": "PDF stream",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Inspection not found"
          },
          "500": {
            "description": "PDF generation failed"
          }
        }
      }
    },
    "/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 templates an inspection can be started from\nare returned (active, with at least one question switched on); 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 templates an inspection can be started from (active, with a question switched on). Template authors may pass false to list every template."
          },
          {
            "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionTemplateSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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"
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Items per page. Values outside 1–100 are clamped, not refused."
          }
        ],
        "responses": {
          "200": {
            "description": "Corrective actions list",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionCorrectiveAction"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "segment_counts": {
                                  "type": "object",
                                  "description": "Badge counts for the native My Actions chips,\ncomputed from the same assignee\nscope as `items` but ignoring the current\n`status`/`overdue` filter. `active` and `open`\nare the same number (pending + in_progress);\n`closed` is completed + cancelled; `overdue`\nis open actions past their due date.\n",
                                  "properties": {
                                    "all": {
                                      "type": "integer"
                                    },
                                    "active": {
                                      "type": "integer"
                                    },
                                    "open": {
                                      "type": "integer"
                                    },
                                    "pending": {
                                      "type": "integer"
                                    },
                                    "in_progress": {
                                      "type": "integer"
                                    },
                                    "completed": {
                                      "type": "integer"
                                    },
                                    "overdue": {
                                      "type": "integer"
                                    },
                                    "closed": {
                                      "type": "integer"
                                    },
                                    "cancelled": {
                                      "type": "integer"
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "`invalid_status` — `status` is not one of the accepted values",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "description": "- `app_disabled` — Inspections is not enabled for the caller's business\n- `app_access_denied` — the caller does not meet the Inspections app's visibility rules\n- `insufficient_permissions` — the token lacks `read:inspections` (\"Required scope: read:inspections\")\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Inspections marketplace app is not configured on this install",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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 — the caller may not update this corrective action, or (`ownership_change_forbidden`) is its assignee changing its due date or priority, or leaving it unassigned, which only a manager, the inspection's inspector or whoever raised it may do"
          },
          "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",
        "description": "When the company requires a photo to complete a corrective action\n(Settings › Inspections, on by default), the action must carry a photo\nof the completed work: send it as the multipart `completion_photo` on\nthis request, or have one already attached. Without it the answer is\n`422 completion_photo_required`. The same rule applies to\n`PATCH /inspections/corrective_actions/{id}` with `status: completed`\n(which takes the multipart `completion_photo` too), and an action cannot\nbe created already completed while it applies. A `sync` / `complete`\nbatch skips such an entry and lists it in `skipped_corrective_actions`.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_notes": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "completion_photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "Photo of the completed work"
                  }
                }
              }
            }
          }
        },
        "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 (`cannot_complete`), a photo is required and none is attached (`completion_photo_required`), or the photo was refused (`completion_photo_invalid`)"
          }
        }
      }
    },
    "/drive/config": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read Drive configuration",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. The same settings and scope capabilities appear in desktop bootstrap. This request fetches no file bytes.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "drive": {
                      "$ref": "#/components/schemas/DriveConfig"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/my_drive": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Find or provision My Drive",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Idempotently provisions the caller’s personal Drive on first use.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "drive": {
                      "$ref": "#/components/schemas/Drive"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/shared": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List items shared with the caller",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DriveItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/drive/recent": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List recently updated accessible files",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DriveItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/drive/search": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Search accessible Drive items",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Results are permission-filtered. q requires at least two characters; names and content modes are explicit.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DriveItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "names",
                "content"
              ]
            }
          }
        ]
      }
    },
    "/drive/people": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Find people for a direct share",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Returns at most 20 active people; q requires at least two characters. No email address is exposed.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/drive/drives": {
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List accessible drives",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "drives": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Drive"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/drive/drives/{drive_id}": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read a drive",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "drive": {
                      "$ref": "#/components/schemas/Drive"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List folder items or Trash",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Listing is metadata only; no thumbnail or signed content URL is returned. Trash rows include deleted_at and restorable. Item shares can grant a folder without access to the drive root.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DriveItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "parent_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trashed",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a folder",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires editor on the destination. Native file bytes use upload sessions with purpose drive.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "kind"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read item metadata",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Requires viewer on the item. Selecting a file does not record a preview or download.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      },
      "patch": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Rename an item",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires editor; a conflicting sibling name is refused.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Move an item to Trash",
        "description": "Requires destructive:drive; Drive must be enabled and accessible to the caller. Requires editor and applies the configured retention policy.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/restore": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Restore an item from Trash",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires editor. A deleted ancestor returns 409; expiry returns 422; name collisions return renamed_from.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/preview": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Explicitly preview file content",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Requires viewer. The original blob scan gate runs before any bytes or URL are returned (423 while held). Text is escaped client-side and capped at 262144 bytes; supported media get a short-lived URL. Unsupported formats return kind web. Only an actual preview records previewed activity.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preview": {
                      "$ref": "#/components/schemas/DrivePreview"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/download": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve an explicit file download",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Requires viewer and a clear scan verdict (423 otherwise). The URL, version_id and byte_size describe the same immutable blob. Native editing binds its base version to this response and validates the received byte count. Never send API bearer credentials to the returned storage URL.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "version_id": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "byte_size": {
                      "type": "integer",
                      "minimum": 0
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/move": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Move an item",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Destination editor required; an explicit null parent means root. Move requires source editor and stays within one drive; copy requires source viewer and may cross drives. Cycles and name collisions are refused by the canonical item mover.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "destination_parent_id"
                ],
                "properties": {
                  "destination_drive_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "destination_parent_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/copy": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Copy an item",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Destination editor required; an explicit null parent means root. Move requires source editor and stays within one drive; copy requires source viewer and may cross drives. Cycles and name collisions are refused by the canonical item mover.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "destination_parent_id"
                ],
                "properties": {
                  "destination_drive_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "destination_parent_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/memberships": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read direct and inherited access",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "memberships": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "principal_type": {
                            "type": "string"
                          },
                          "principal_id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "principal_name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "viewer",
                              "commenter",
                              "editor",
                              "manager"
                            ]
                          },
                          "inherited": {
                            "type": "boolean"
                          },
                          "no_access_reason": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "left",
                              "deleted",
                              null
                            ]
                          }
                        }
                      }
                    },
                    "inherited_audiences": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "everyone",
                          "workspace_owners",
                          "workspace_members"
                        ]
                      }
                    },
                    "inherited_memberships": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "principal_type": {
                            "type": "string"
                          },
                          "principal_id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "principal_name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "viewer",
                              "commenter",
                              "editor",
                              "manager"
                            ]
                          },
                          "inherited": {
                            "type": "boolean"
                          },
                          "no_access_reason": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "left",
                              "deleted",
                              null
                            ]
                          }
                        }
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Grant or change direct access",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires manager. Chat-room viewer grants are made only through send_to_chat with explicit grant_access.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "membership": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "principal_type": {
                          "type": "string"
                        },
                        "principal_id": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "principal_name": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "viewer",
                            "commenter",
                            "editor",
                            "manager"
                          ]
                        },
                        "inherited": {
                          "type": "boolean"
                        },
                        "no_access_reason": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "left",
                            "deleted",
                            null
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "principal_type": {
                    "type": "string",
                    "enum": [
                      "User",
                      "NotificationRecipientGroup"
                    ]
                  },
                  "principal_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "role": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/memberships/{membership_id}": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "membership_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "delete": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Revoke direct access",
        "description": "Requires destructive:drive; Drive must be enabled and accessible to the caller. Requires manager. Inherited access is managed at its source; another grant may still allow access.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/external_links": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List external links",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. Requires manager.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "external_links": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "public_url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "download_count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create an external viewer link",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires manager and the external-sharing capability. Password and lifetime requirements come from configuration and are rechecked server-side.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "external_link": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "public_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "download_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "external_link": {
                    "type": "object",
                    "properties": {
                      "role": {
                        "type": "string",
                        "enum": [
                          "viewer"
                        ]
                      },
                      "password": {
                        "type": "string"
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "download_limit": {
                        "type": "integer",
                        "minimum": 1
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/external_links/{link_id}": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "link_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "delete": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Revoke an external link",
        "description": "Requires destructive:drive; Drive must be enabled and accessible to the caller. Requires manager.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/versions": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List version history",
        "description": "Requires read:drive; Drive must be enabled and accessible to the caller. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DriveVersion"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/DrivePagination"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/drive/drives/{drive_id}/items/{id}/versions/{version_id}/restore": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "version_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Restore a previous version",
        "description": "Requires write:drive; Drive must be enabled and accessible to the caller. Requires editor and readable stored bytes. Restoring changes the current version without creating or pruning a version.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/DriveItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/chat_reach": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Check who can open a file in selected chats",
        "description": "Requires write:drive and write:chat; Drive must be enabled and accessible to the caller. Requires membership and permission to post in every selected chat. Missing-reader counts are exact and include current Drive-app access. This read never grants access.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rooms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "missing_count": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "can_grant": {
                            "type": "boolean"
                          },
                          "manager_name": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room_ids"
                ],
                "properties": {
                  "room_ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/drives/{drive_id}/items/{id}/send_to_chat": {
      "parameters": [
        {
          "name": "drive_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send a Drive file link to chats",
        "description": "Requires write:drive and write:chat; Drive must be enabled and accessible to the caller. The same request_id is idempotent per sender/file/chat. Partial refusals are returned per chat. Sending alone never changes access. Explicit grant_access requires file manager and grants viewer to current members of each chat; leaving that chat removes this route to access. The durable message stores file metadata only, alongside a normal same-tenant web link.",
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "integer",
                            "minimum": 1
                          }
                        }
                      }
                    },
                    "refused": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room_ids",
                  "request_id"
                ],
                "properties": {
                  "room_ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "integer",
                      "minimum": 1
                    }
                  },
                  "request_id": {
                    "type": "string",
                    "minLength": 16,
                    "maxLength": 100
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "grant_access": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        }
      }
    },
    "/drive/save_attachment": {
      "post": {
        "tags": [
          "Drive"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Save an independent chat attachment copy",
        "description": "Requires write:drive and read:chat; Drive must be enabled and accessible to the caller. Requires current chat membership, an available unmasked message and editor on the destination. A background job streams, rescans and stores a new independent blob. Poll the returned upload session; no file bytes traverse the renderer. Reuse request_id for an ambiguous retry; a terminal failure can be retried with a fresh request id.",
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_session": {
                      "type": "object",
                      "properties": {
                        "token": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "composing",
                            "completed",
                            "failed",
                            "expired"
                          ],
                          "description": "`open` (accepting parts) → `composing` (assembling) → `completed` or\n`failed`. `expired` is terminal too: the session ran past\n`expires_at` before it finished, or had failed and was kept past it.\n"
                        },
                        "filename": {
                          "type": "string"
                        },
                        "byte_size": {
                          "type": "integer",
                          "format": "int64"
                        },
                        "part_size": {
                          "type": "integer",
                          "description": "Bytes per part (the last part carries the remainder)."
                        },
                        "parts_count": {
                          "type": "integer"
                        },
                        "uploaded_parts": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          },
                          "description": "Part numbers stored so far (while `open`)."
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "result": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Present once `completed` — what the door made of the file."
                        },
                        "error": {
                          "type": "string",
                          "description": "Present when `failed`, and kept on an `expired` session that had failed."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "App, token scope or record permission refused."
          },
          "404": {
            "description": "The tenant-scoped record is unavailable."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message_id",
                  "attachment_id",
                  "drive_id",
                  "request_id"
                ],
                "properties": {
                  "message_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "attachment_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "drive_id": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "parent_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "request_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/uploads/sessions": {
      "post": {
        "tags": [
          "Upload Sessions"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Open an upload session",
        "description": "Declares the file and where it is going. The door authorizes the target\nand applies its size/type policy HERE, before any byte is sent\n(403 `forbidden`, 404 `not_found`, 422 `too_large` / `unsupported_type`).\n\nThe response gives `part_size` and `parts_count`: split the file into\nconsecutive parts of exactly `part_size` bytes (the last part carries\nthe remainder) and PUT each one. Run up to 4 parts in parallel. An\nunfinished session expires 24 hours after it was opened, and one user\nmay hold at most 20 open sessions in a business at a time\n(429 `too_many_uploads` — finish or DELETE one first).\n\n`context` by purpose. Unknown keys are dropped.\n\n* `drive` — `drive_id` (required), `parent_id`, `rel_path` (the file's\n  webkitRelativePath, for an upload-folder drop: the folder chain is\n  created under `parent_id`).\n* `ai_notepad` — `meeting_id` (required).\n* `recorder` — `recording_id` (required).\n* `drive_version` — `drive_id`, `item_id`, `base_version_id`, optional\n  `change_summary` and `force`. Requires item editor and write:drive.\n  A stale base returns 409 conflict before bytes; the base and permission\n  are checked again at publication. `force: \"true\"` is an explicit user\n  choice; upload-session context scalar values are strings.\n  The completed result carries drive_item_id and version_id. A durable\n  publication receipt makes a retry after interrupted completion safe.\n* `chat` — `room_id` (required), `media_type`, `width_px`, `height_px`.\n* `news_feed` — `feed_id`, `parent_type`, `parent_id`, `media_type`,\n  `alt_text`, `width_px`, `height_px`.\n* `power_tables_import` — `table_id` (the table being imported into,\n  when there is one; its owning app's admins may upload for it).\n* `hub_migration` — no context. A MangoApps Hub export bundle (a\n  `.tar`, up to 20 GB) for the Hub → OneMango migration, sent by the\n  business's own service account or one of its business administrators\n  with `write:data_migration`. Receiving a bundle imports nothing; the\n  import is a separate, previewed step. Available only where the\n  migration is switched on (403 `not_available` elsewhere, including\n  production for now); a token of another business is refused (403\n  `forbidden`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "purpose",
                  "filename",
                  "byte_size"
                ],
                "properties": {
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "drive",
                      "drive_version",
                      "ai_notepad",
                      "recorder",
                      "chat",
                      "news_feed",
                      "power_tables_import",
                      "hub_migration"
                    ]
                  },
                  "filename": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "content_type": {
                    "type": "string",
                    "example": "video/mp4"
                  },
                  "byte_size": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Exact size of the file in bytes (max 20 GB, and the door's own limit)."
                  },
                  "context": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session opened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_session": {
                      "$ref": "#/components/schemas/UploadSession"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the door's write scope (`insufficient_permissions`, \"Required scope: …\"), the caller may not upload to that target, or the door's app is not enabled for the business / not available to this user (`app_not_enabled`, `app_not_accessible`, `forbidden`), or the door is not available in this environment (`not_available`)."
          },
          "404": {
            "description": "The target no longer exists."
          },
          "422": {
            "description": "Refused: `too_large`, `unsupported_type`, `unknown_purpose`, `invalid_upload`."
          },
          "429": {
            "description": "`too_many_uploads` — this user already holds the maximum number of open sessions. Finish or abort one."
          }
        }
      }
    },
    "/uploads/sessions/{token}": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Upload Sessions"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Session status",
        "description": "While `status` is `open`, `uploaded_parts` lists the part numbers that\nhave landed — after a dropped connection or an app restart, send only\nthe ones that are missing. After `complete`, poll this until `status`\nis `completed` (read `result`) or `failed` (read `error`).\n\n`expired` is the third terminal state: an unfinished session that ran\npast `expires_at`, or a failed one kept past it. Its parts are gone —\nopen a new session. A failed session that expires keeps its `error`.\n\n`result` by purpose:\n\n* `drive` — `{ drive_id, drive_item_id, parent_id, name }`. The file is\n  already in Drive.\n* `ai_notepad` / `recorder` — `{ meeting_id` or `recording_id,\n  signed_blob_id, complete_with }`. Post that `signed_blob_id` to the\n  endpoint named in `complete_with` (the app's existing\n  `…/audio/complete` or `…/media/complete`) within one hour to attach\n  the file and start processing. Until you do, nothing owns the file.\n* `chat` / `news_feed` — `{ media }`, the same media object the door's\n  own `media#create` returns; post it as you would any attachment.\n* `power_tables_import` — `{ blob_signed_id, filename }`. Carry the\n  `blob_signed_id` into the import wizard's next step; it is scoped to\n  this tenant's imports and the staged file is purged after 24 hours if\n  the wizard is abandoned.\n* `hub_migration` — `{ bundle_id, filename, byte_size }`. The bundle is\n  kept for the business; an administrator previews and runs its import\n  separately.\n",
        "responses": {
          "200": {
            "description": "The session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_session": {
                      "$ref": "#/components/schemas/UploadSession"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such session for this user."
          }
        }
      },
      "delete": {
        "tags": [
          "Upload Sessions"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Abort a session",
        "description": "Discards a session that is still `open` (with every part already sent)\nor one that has `failed`. A session being assembled is left to finish,\nand a `completed` one is NOT deleted — for `ai_notepad` and `recorder`\nits file is unattached until you call the door's own `complete` leg, and\nthe row is what the platform's cleanup reads to find that file. Both are\na 204 no-op, and the session stays readable.\n",
        "responses": {
          "204": {
            "description": "Aborted, or a no-op for a session that is not `open`/`failed`."
          },
          "404": {
            "description": "No such session for this user."
          }
        }
      }
    },
    "/uploads/sessions/{token}/parts/{part_number}": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "part_number",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "put": {
        "tags": [
          "Upload Sessions"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Send one part",
        "description": "The part's raw bytes as the request body (`Content-Type:\napplication/octet-stream`); a multipart `chunk` field is accepted too.\nThe body must be exactly the part's size — `part_size`, or the\nremainder for the last part (422 `part_size_mismatch` otherwise).\nRe-sending a part replaces it, so a failed or uncertain part is simply\nsent again. 503 `storage_unavailable` is retryable.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Part stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "part_number": {
                      "type": "integer"
                    },
                    "byte_size": {
                      "type": "integer",
                      "format": "int64"
                    },
                    "uploaded_parts": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`session_closed` (no longer accepting parts) or `part_in_flight`."
          },
          "410": {
            "description": "`session_expired` — open a new session."
          },
          "422": {
            "description": "`part_out_of_range`, `part_missing` or `part_size_mismatch`."
          },
          "503": {
            "description": "`storage_unavailable` — send the part again."
          }
        }
      }
    },
    "/uploads/sessions/{token}/complete": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Upload Sessions"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Assemble the file",
        "description": "Call once every part has landed. The server re-checks the caller's\naccess to the target, assembles the parts in the background, reads the\nfile's real type and applies the door's policy to it, and hands the\nfile to the door. Returns 202 immediately — poll the session for the\noutcome.\n\nSAFE TO CALL AGAIN, including concurrently: the session is claimed\natomically, so only the first call assembles and a second is answered\n202 with the current state. A refusal the door makes while assembling\n(the file is not what it claimed, access was revoked) is reported on the\nsession as `failed`, not on this response.\n",
        "responses": {
          "202": {
            "description": "Assembling (or already done)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_session": {
                      "$ref": "#/components/schemas/UploadSession"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The caller may no longer upload to that target — the door re-authorizes here."
          },
          "404": {
            "description": "No such session for this user, or the target no longer exists."
          },
          "409": {
            "description": "`session_closed` — the session has already failed or expired; open a new one."
          },
          "422": {
            "description": "`parts_missing` — `details.missing_parts` lists them."
          }
        }
      }
    },
    "/inspections/direct_uploads": {
      "post": {
        "deprecated": true,
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "DEPRECATED: request a signed direct-upload URL for an inspection photo or video",
        "description": "**Deprecated — post the file part on the write itself instead.** Every\nendpoint that took a `blob_signed_id` from here now accepts the bytes\nas multipart file parts in the SAME request, on every storage provider,\nwith no blob minted before a record owns it and nothing to sweep:\n\n| Instead of a signed id on… | send |\n|---|---|\n| `POST /inspections/inspections/{id}/items/{id}/media` | `files[]` (or a single `file`) |\n| `POST /inspections/inspections` | `media_uploads[][file]` |\n| `POST /inspections/inspections/{id}/sync` | `media_uploads[][file]` |\n| `POST /inspections/sweeps/{id}/exceptions` | `photos[]` |\n| `PATCH /inspections/sweeps/{id}/exceptions/{id}` | `photos[]` |\n\nThis endpoint keeps working for native clients already in the field.\nEvery response carries `Deprecation: true`, a `Link` header naming the\nsuccessor, and a `deprecation` object in the body.\n\nWraps 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\ndocument (PDF, Word, Excel, PowerPoint, plain text or CSV), set the\ntop-level `non_media` flag to true — that adds those document types\nto the allow-list (the size cap still applies, ≤ 50 MB for non-photo\ntypes).\n\nThe `signed_id` is accepted only where the bytes are virus-scanned\nbefore they attach: a photo or video at the Inspections doors above,\nand any allowed file at a form submission's or a broadcast's /\nalert's media fields (the Inspections doors refuse a document). No\nother endpoint that takes a signed blob id will take it.\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"
                    },
                    "deprecation": {
                      "type": "object",
                      "description": "Names the multipart successor for this endpoint.",
                      "properties": {
                        "deprecated": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "successor": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`invalid_type` / `too_large` — content_type not allowed or file too large; `invalid_blob` — the blob metadata is missing a key or was rejected"
          }
        }
      }
    },
    "/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. Deprecated: it will be removed from this payload in the next API version.",
                      "deprecated": true,
                      "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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\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
                    },
                    "inspection_server_version": {
                      "type": "string",
                      "description": "The inspection's `server_version` after this answer. A score-changing answer re-stamps the inspection, so an offline client replaces its stored inspection base with this value."
                    },
                    "score_breakdown": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": true
                    },
                    "failure_details_missing": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "severity",
                          "comment",
                          "photo",
                          "action_item"
                        ]
                      },
                      "description": "For an item graded failed on a template that requires failure details, what it still lacks — so the app opens its failure sheet as Fail does. Completing refuses (`failure_details_required`) until they are sent. Empty when none.",
                      "example": [
                        "severity",
                        "comment"
                      ]
                    }
                  }
                }
              }
            }
          },
          "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 photos or videos to an inspection item",
        "description": "Single canonical media-upload endpoint backed by `MediaItem`.\n\n1. **Multipart (use this)** — `files[]` for several files, or a single\n   `file`, plus an optional `kind`. The bytes enter through Rails, are\n   pushed to storage concurrently and are attached in the same request;\n   nothing exists in storage that this item does not own by the end of\n   it. Refusals are ALL-OR-NOTHING: one unsupported or oversize file\n   fails the call and nothing is kept. The type is checked against the\n   REAL bytes as well as the declared one, so an executable renamed\n   `.png` is refused (422 `invalid_type`) and its bytes are purged in\n   the same request.\n2. **Signed blob (deprecated)** — `{ blob_signed_id, kind }`, for\n   native clients that already called\n   `POST /inspections/direct_uploads`. Still works; the response\n   carries `Deprecation: true`.\n\n`kind` is optional when `content_type` starts with `image/` or\n`video/`; explicit otherwise. Photos are capped at 20 MB, videos at\n50 MB, and at most 20 file parts per request.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The file parts. Use `file` for a single upload."
                  },
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "video"
                    ]
                  },
                  "blob_signed_id": {
                    "type": "string",
                    "deprecated": true
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "blob_signed_id": {
                    "type": "string",
                    "deprecated": true
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "video"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Media attached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media_item": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/InspectionMediaItem"
                        }
                      ],
                      "description": "The FIRST attached item, always present — a single-file client reading this key is unaffected by the batch shape."
                    },
                    "media_items": {
                      "type": "array",
                      "description": "Every item attached by this request, in submission order.",
                      "items": {
                        "$ref": "#/components/schemas/InspectionMediaItem"
                      }
                    },
                    "media_count": {
                      "type": "integer"
                    },
                    "already_attached": {
                      "type": "integer",
                      "description": "Present on a multipart call when one or more of the posted files were already on this item (same bytes) — a retried upload. Those files are echoed in `media_items` as the existing rows and are not attached twice."
                    },
                    "item": {
                      "$ref": "#/components/schemas/InspectionItem"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "`invalid_type` / `too_large` / `no_file` / `invalid_file` — the file itself was refused; `upload_blocked` — the virus scan found malware (a `blob_signed_id` upload is deleted); `upload_incomplete` — a `blob_signed_id` upload does not match its declared checksum"
          },
          "503": {
            "description": "`storage_unavailable` — file storage could not be reached; or `scanner_unavailable` — uploads are paused while virus scanning is unavailable. Nothing was kept; retry the call"
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\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"
          }
        }
      ],
      "get": {
        "tags": [
          "Inspections"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List corrective actions on one inspection",
        "description": "The same list as `GET /inspections/corrective_actions` (same action,\nsame filters, same item shape), narrowed to the corrective actions\nwhose `inspection_id` is the path's `inspection_id`. Because of that\nnarrowing, Visit Log plans of correction (parented by a visit\nfinding) and actions raised by another app (no parent inspection)\nnever appear here, and `meta.segment_counts` counts only this\ninspection's actions.\n\nThe inspection itself is **not** looked up: an unknown inspection id,\nor one belonging to another business, answers **200 with an empty\n`items`** (never 404 — the list is always scoped to the caller's\nbusiness, so nothing from another tenant is returned). A non-numeric\nid also yields an empty list (a value with leading digits, such as\n`12abc`, is read as that number). Read access to the inspection is not\nchecked either; the caller sees only the rows the assignee scope\nbelow lets them see.\n\nAssignee scope, as on the top-level list:\n- default — actions on this inspection assigned to the caller, and\n  only **open** ones (`pending` + `in_progress`) unless `status` or\n  `overdue` is given;\n- `assigned_to_me=false` — actions on this inspection the caller is\n  assigned to **or** created (any status);\n- `team=true` (manager, business admin or Inspections app admin) —\n  every action on this inspection, but only when the inspection is\n  inside the caller's team scope (`Inspections::TeamScopeService`);\n  otherwise the list is empty. Non-managers who send `team=true` get\n  the default personal scope.\n\n`for_inspection_id` is redundant on this route: it is applied as an\nadditional `inspection_id` condition, so a value that differs from\nthe path id yields an empty list.\n\nOrdering: open actions first, then `due_date` ascending, then\n`created_at` descending, then `id` descending.\n\n**Required Scopes:** `read:inspections` (GET and HEAD). A token\nwithout it is refused **403** `insufficient_permissions`\n(\"Required scope: read:inspections\"). Session-authenticated requests\npass.\n",
        "parameters": [
          {
            "name": "assigned_to_me",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "When false, also surfaces actions the caller created (and drops\nthe open-only default). Ignored when `team=true` resolves to\nmanager scope.\n"
          },
          {
            "name": "for_inspection_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Redundant here — ANDed with the path `inspection_id`; a different\nvalue returns an empty list.\n"
          },
          {
            "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). Any other value is refused 400\n`invalid_status`.\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 to every action on this inspection\nwhen the inspection is inside the caller's team scope.\nNon-managers who pass `team=true` silently get the personal scope.\n"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Items per page. Values outside 1–100 are clamped, not refused."
          }
        ],
        "responses": {
          "200": {
            "description": "Corrective actions on the inspection (empty when the inspection is unknown, another business's, or outside the caller's scope)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "required": [
                        "items"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionCorrectiveAction"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/CollectionPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "segment_counts": {
                                  "type": "object",
                                  "description": "Badge counts for the native My Actions chips,\ncomputed from the same inspection + assignee\nscope as `items` but ignoring the current\n`status`/`overdue` filter. `active` and `open`\nare the same number (pending + in_progress);\n`closed` is completed + cancelled; `overdue`\nis open actions past their due date.\n",
                                  "properties": {
                                    "all": {
                                      "type": "integer"
                                    },
                                    "active": {
                                      "type": "integer"
                                    },
                                    "open": {
                                      "type": "integer"
                                    },
                                    "pending": {
                                      "type": "integer"
                                    },
                                    "in_progress": {
                                      "type": "integer"
                                    },
                                    "completed": {
                                      "type": "integer"
                                    },
                                    "overdue": {
                                      "type": "integer"
                                    },
                                    "closed": {
                                      "type": "integer"
                                    },
                                    "cancelled": {
                                      "type": "integer"
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "`invalid_status` — `status` is not one of the accepted values",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — bearer token missing or invalid"
          },
          "403": {
            "description": "- `app_disabled` — Inspections is not enabled for the caller's business\n- `app_access_denied` — the caller does not meet the Inspections app's visibility rules\n- `insufficient_permissions` — the token lacks `read:inspections` (\"Required scope: read:inspections\")\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found` — the Inspections marketplace app is not configured on this install (never for an unknown inspection)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "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. Deprecated: it will be removed from this payload in the next API version.",
                      "deprecated": true,
                      "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionScheduleSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionCycleItem"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/InspectionCycleSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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"
                                  }
                                },
                                "items": {
                                  "type": "object",
                                  "description": "Per-item cycles only: the item register the caller may see.\n`unstarted` items have no inspection yet (unclaimed, or nobody\ncould be assigned). `not_inspected` is the same count once the\ncycle is closed (completed or archived) — they can no longer be\nclaimed or assigned — and 0 while it is open.\n",
                                  "properties": {
                                    "total": {
                                      "type": "integer"
                                    },
                                    "completed": {
                                      "type": "integer"
                                    },
                                    "unstarted": {
                                      "type": "integer"
                                    },
                                    "not_inspected": {
                                      "type": "integer"
                                    },
                                    "by_status": {
                                      "type": "object",
                                      "additionalProperties": {
                                        "type": "integer"
                                      }
                                    }
                                  }
                                },
                                "sweeps": {
                                  "type": "object",
                                  "description": "Coverage-sweep cycles only: the sweeps the caller may see. The cycle\ncan be signed off once every sweep is. `ready_for_sign_off` counts\nsweeps with every aisle covered that still wait on their sign-off.\n",
                                  "properties": {
                                    "total": {
                                      "type": "integer"
                                    },
                                    "signed_off": {
                                      "type": "integer"
                                    },
                                    "ready_for_sign_off": {
                                      "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_inactive` — the cycle is no longer active; `no_template` —\nthe cycle has no inspection template configured;\n`cycle_not_self_startable` — the cycle's inspections come from its\nitems, claimed or assigned one at a time (claim an item with\n`POST /inspections/cycles/{cycle_id}/items/{id}/claim`); the\nmessage says why.\n",
            "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. A handed-off aisle (in progress, nobody on it — the summary's `handed_off_aisles`) is taken before a fresh one, so the aisle returned may already be in progress, with cells. Pass `aisle_id` to pick up one specific handed-off aisle.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aisle_id": {
                    "type": "integer",
                    "description": "Pick up this handed-off aisle (optional)."
                  }
                }
              }
            }
          }
        },
        "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). With `aisle_id`: `aisle_taken` — another inspector is on that aisle; `aisle_done` — it was covered or skipped since it was listed; `aisle_not_found` — it is not on this sweep. `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).\n\nA batch that carries `resets` erases recorded results as `reset_bay` /\n`reset_aisle` do, so it is delete-level (owner decision 2026-10-04,\n#6721): on top of `write:inspections` it requires the\n`destructive:inspections` scope.\n",
        "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"
                        }
                      }
                    },
                    "skipped": {
                      "type": "array",
                      "description": "Every submitted element that was NOT applied (empty when all applied). `index` is the element's 0-based position in the array it was submitted in (always 0 for frontier).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "cell",
                              "exception",
                              "reset",
                              "frontier"
                            ]
                          },
                          "index": {
                            "type": "integer"
                          },
                          "code": {
                            "type": "string",
                            "description": "invalid_element | aisle_not_found | out_of_range | validation_failed | save_failed | action_failed"
                          },
                          "message": {
                            "type": "string",
                            "description": "Safe to show the inspector; never raw exception text."
                          }
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Every submitted element that WAS applied (counted in `applied`) but not whole — a finding saved with fewer photos than it carried (empty when none). Do not re-send the finding; attach the missing photos to `exception_id`. A finding whose `aisle_id` names no aisle on this sweep is not applied and is in `skipped` as `aisle_not_found`, like a cell or reset; a finding sent with no `aisle_id` is saved without an aisle.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "exception"
                            ]
                          },
                          "index": {
                            "type": "integer"
                          },
                          "code": {
                            "type": "string",
                            "description": "`photos_not_attached`; or, beside it, `upload_blocked` — the virus scan found malware in one of the finding's `blob_signed_ids` photos, which is deleted and not attached (do not re-send it) — or `upload_incomplete` — that upload does not match its declared checksum (upload it again)"
                          },
                          "message": {
                            "type": "string",
                            "description": "Safe to show the inspector."
                          },
                          "exception_id": {
                            "type": "integer",
                            "description": "The saved finding."
                          },
                          "photos_sent": {
                            "type": "integer"
                          },
                          "photos_attached": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`sweep_locked` — sweep is signed off"
          },
          "503": {
            "description": "`scanner_unavailable` — uploads are paused while virus scanning is unavailable, or `storage_unavailable` — a finding's `blob_signed_ids` photo could not be read. Nothing was written; retry the call"
          }
        }
      }
    },
    "/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. Open critical findings do not block (the sweep's `untracked_critical_count` is the warning to show). 422 only when the sweep is already pending review; 409 once it is signed off.",
        "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"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`sweep_locked` — the sweep is already signed off"
          },
          "422": {
            "description": "`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)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\n",
        "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)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\n",
        "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 its open findings; resolved and corrective ones stay)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\n",
        "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,
                      "description": "The aisle the inspector rolls onto — never one another inspector is on."
                    },
                    "done": {
                      "type": "boolean",
                      "description": "True only when no aisle is left uncovered. `next_aisle_id: null` with `done: false` means the aisles left are being swept by others."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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 as `photos[]` multipart file parts (use this — one request, nothing pre-uploaded), as photo_ids (pending inbox MediaItem ids), or as blob_signed_ids (DEPRECATED: bytes PUT via /inspections/direct_uploads). All three are additive. 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"
                  },
                  "kind": {
                    "type": "string",
                    "description": "`noteworthy` logs a notable exception — position-less, located by `location_text` (required then); omit for a rack finding."
                  },
                  "location_text": {
                    "type": "string",
                    "description": "Where a notable exception is (\"Stairwell B, east end\"). Required when `kind` is noteworthy."
                  },
                  "component": {
                    "type": "string",
                    "description": "Required on a rack finding. Optional on a notable exception — none or blank is filed as \"General\" (the bundle's notable_form_schema.component_default)."
                  },
                  "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"
                    },
                    "deprecated": true,
                    "description": "DEPRECATED — signed Active Storage blob ids from /inspections/direct_uploads"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "photos": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The finding's photo file parts, attached in THIS request — no pre-uploaded blob and nothing to sweep. Every other field above may be sent form-encoded alongside them. Refused files (unsupported type, checked against the real bytes, or over the per-type cap) fail the call with 422 (`invalid_type` / `too_large` / `no_file` / `invalid_file` — the same vocabulary as the item media door) and nothing is kept."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "503": {
            "description": "`storage_unavailable` — file storage could not be reached; or `scanner_unavailable` — uploads are paused while virus scanning is unavailable. Nothing was kept; retry the call"
          },
          "201": {
            "description": "Finding logged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    },
                    "corrective_action_created": {
                      "type": "boolean"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "The finding was saved but not whole — fewer photos attached than were sent (`photos_not_attached`); empty when none. Attach the missing photos to `exception_id` rather than logging the finding again.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "description": "photos_not_attached"
                          },
                          "message": {
                            "type": "string",
                            "description": "Safe to show the inspector."
                          },
                          "exception_id": {
                            "type": "integer"
                          },
                          "photos_sent": {
                            "type": "integer"
                          },
                          "photos_attached": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`aisle_not_found` — `aisle_id` names no aisle on this sweep (the same refusal #sync gives as a skip); `upload_blocked` — the virus scan found malware in a `blob_signed_ids` photo (it is deleted); `upload_incomplete` — a `blob_signed_ids` upload does not match its declared checksum. Nothing was saved"
          }
        }
      }
    },
    "/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 (`photos[]` multipart file parts, photo_ids, or the DEPRECATED 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"
                    },
                    "deprecated": true,
                    "description": "DEPRECATED — signed Active Storage blob ids (additive)"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "photos": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The finding's photo file parts, attached in THIS request — no pre-uploaded blob and nothing to sweep. Every other field above may be sent form-encoded alongside them. Refused files (unsupported type, checked against the real bytes, or over the per-type cap) fail the call with 422 (`invalid_type` / `too_large` / `no_file` / `invalid_file`) and nothing is kept. A photo already on the finding (same bytes — a retried call) is skipped rather than attached twice."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "The edit was saved but not whole — fewer of the newly sent photos attached than were sent (`photos_not_attached`); empty when none. A photo already on the finding is not counted. Attach the missing photos to `exception_id` again.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "description": "photos_not_attached"
                          },
                          "message": {
                            "type": "string",
                            "description": "Safe to show the inspector."
                          },
                          "exception_id": {
                            "type": "integer"
                          },
                          "photos_sent": {
                            "type": "integer"
                          },
                          "photos_attached": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Update failed, a photo was refused (`upload_blocked` when the virus scan found malware in a `blob_signed_ids` photo, which is deleted; `upload_incomplete` when one does not match its declared checksum), or `aisle_not_found` — `aisle_id` names no aisle on this sweep (nothing was changed)"
          },
          "503": {
            "description": "`storage_unavailable` — file storage could not be reached; or `scanner_unavailable` — uploads are paused while virus scanning is unavailable. Nothing was kept; retry the call"
          },
          "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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:inspections` scope.\n",
        "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}/reopen": {
      "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": "Reopen a resolved finding",
        "description": "The undo for resolve. Only a finding whose status is `resolved` goes back to open; any other answers 422 `not_resolved`.",
        "responses": {
          "200": {
            "description": "Finding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exception": {
                      "$ref": "#/components/schemas/InspectionSweepException"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The finding is not resolved"
          }
        }
      }
    },
    "/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\n`meta.rules` carries the organization's Messages rules as they apply to\nthe caller, so a client can show \"New message\", the group picker and the\nattach button only when they will work, and say how long messages are\nkept, instead of learning each rule from a refusal.\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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MessagingThread"
                          }
                        },
                        "meta": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/MessagingPaginationMeta"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "rules": {
                                  "$ref": "#/components/schemas/MessagingRules"
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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)."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Optional subject line, as the web and phone composers ask for. It names only a conversation this call creates — a 1:1 that already exists keeps its own subject.\n"
                  },
                  "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": "`thread_not_created` — the caller may not start a new conversation (`error.message` names the organization's rule, e.g. \"You can't start a new conversation. Managers and above can start a new conversation; everyone can reply.\"; replying to an existing conversation is never blocked by it — see `meta.rules.can_start` on GET /messaging/threads) or group messages are turned off; or `information_barrier` — the company's information barriers keep the caller and a recipient apart (the existing one-to-one is not reopened either)"
          },
          "503": {
            "description": "`information_barrier_unavailable` — who is in an information barrier's group couldn't be read just now; try again"
          },
          "422": {
            "description": "No recipients (`no_recipients`), a recipient unavailable (`recipients_unavailable`) or participant cap exceeded. A recipient who is no longer active is named in `error.message` and listed in `error.details.departed_user_ids`."
          }
        }
      }
    },
    "/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, and leaves out the people the company's information\nbarriers keep the caller from (a conversation with them is refused).\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/reaction_options": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Reaction emoji a user may add to a message",
        "description": "The static allowlist of emoji a caller can add as a reaction on a Direct\nMessage, each with a display label — the same set ReactionsController\nvalidates against and the web thread reaction bar renders. Identical for\nevery message (no thread/message context); fetch once to build a reaction\npicker instead of hardcoding the set.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The applicable reactions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reactions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagingReactionOption"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages disabled for the organization"
          }
        }
      }
    },
    "/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}/unread": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Mark a conversation unread",
        "description": "The inverse of PATCH `/messaging/threads/{id}/read`, and the same write as\nthe web Messages \"Mark unread from here\": the CALLER's read marker is moved\nback to just before `message_id`, so that message and every later message\nsomeone else wrote count as unread. Without `message_id` it moves back\nbefore the newest message someone else wrote (the caller's own messages\nnever count as unread). The conversation then shows its `unread_count` in\nGET `/messaging/threads` and the badge total; opening it (GET\n`/messaging/threads/{id}` with a write-capable token) or PATCH `/read`\nclears it again. Per-caller; nobody else's read state changes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message_id": {
                    "type": "integer",
                    "description": "A message in this conversation to mark unread from. Omit to mark unread from the newest message someone else wrote."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Marked unread",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "unread_count": {
                      "type": "integer",
                      "example": 3,
                      "description": "The conversation's unread messages for the caller after the change."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled or insufficient scope (needs write:messages)"
          },
          "404": {
            "description": "Conversation not found (or caller not a participant), or `message_id` is not a message in it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "thread_not_found",
                        "message_not_found"
                      ]
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "No `message_id` was given and nobody else has written in the conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "example": "nothing_to_mark_unread"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:messages` scope.\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, insufficient scope (needs write:messages), group messages turned off for the organization, or the caller may not start conversations (who_can_initiate) — `error.message` says which; or `information_barrier` — the company's information barriers keep someone being added apart from someone already in it, or from another person being added (nobody is added)"
          },
          "503": {
            "description": "`information_barrier_unavailable` — who is in an information barrier's group couldn't be read just now; try again"
          },
          "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
            }
          },
          {
            "name": "around_id",
            "in": "query",
            "description": "Open the conversation AT this message (a mention, a search hit), as the web and phone pages do: a window of `per_page` messages, newest first — half at or before it (it included), half after it. Wins over `before_id`. The `cursor` then carries `around_id`, `next_before_id` (send it as `before_id` to page older), `has_more_before` and `has_more_after`; for \"Back to latest\", request with no cursor. 404 `message_not_found` for a message not in this conversation.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated messages",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MessagingMessage"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/MessagingPaginationMeta"
                        },
                        "cursor": {
                          "type": "object",
                          "description": "Present only when `before_id` or `around_id` was given (replaces `meta`).",
                          "properties": {
                            "before_id": {
                              "type": "integer"
                            },
                            "around_id": {
                              "type": "integer",
                              "description": "Only with around_id — the message the window is centred on."
                            },
                            "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",
                              "description": "With before_id — older messages remain."
                            },
                            "has_more_before": {
                              "type": "boolean",
                              "description": "With around_id — older messages remain."
                            },
                            "has_more_after": {
                              "type": "boolean",
                              "description": "With around_id — newer messages remain (\"Back to latest\")."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Conversation not found",
            "or (with around_id) message_not_found": null
          }
        }
      },
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Post a message (text + attachments)",
        "description": "multipart/form-data — `body` text and/or repeated `attachments[]` file\nparts. When file uploads are disabled for the business, a post carrying\nattachments is refused whole with 422 (\"File attachments are disabled\nfor your organization\") — the body is not posted without them.\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"
          },
          "403": {
            "description": "`information_barrier` — a one-to-one conversation the company's information barriers keep apart takes no new messages"
          },
          "422": {
            "description": "Empty message, not a participant, or attachments sent while file uploads are disabled"
          }
        }
      }
    },
    "/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"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`information_barrier` — a one-to-one conversation the company's information barriers keep apart takes no edits (deleting your own message still works)"
          },
          "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)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:messages` scope.\n",
        "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/{id}/forward": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Forward a message into another conversation",
        "description": "Forwards the message at `{id}` (in a conversation the caller belongs to)\nto one or more recipients — parity with the web \"Forward\" link. The\ndestination receives the quoted original (\"On <date>, <author> wrote:\n> …\") and, when the source thread has a subject, \"Fwd: <subject>\".\n\nRecipients are chosen by the caller (`user_ids[]`, or the legacy single\n`user_id`): 1 recipient reuses/opens the idempotent 1:1 conversation, 2+\nopen a group. `body` is an OPTIONAL note added above the quote. The\nORIGINAL ATTACHMENTS ARE NOT CARRIED — identical to the web forward.\n\nGoverned exactly like a hand-typed message (who_can_initiate / group-DMs\ntoggles, @mentions against the destination's members, broadcast and\nnotifications). Honors `Idempotency-Key` — a retry replays the first\nresponse instead of forwarding twice.\n",
        "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": [
                  "user_ids"
                ],
                "properties": {
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Recipient user ids. 1 => 1:1, 2+ => group."
                  },
                  "user_id": {
                    "type": "integer",
                    "description": "Legacy single recipient (merged with user_ids)."
                  },
                  "body": {
                    "type": "string",
                    "description": "Optional note added above the quoted original."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message forwarded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    },
                    "thread_id": {
                      "type": "integer",
                      "description": "The destination conversation (new or reopened)."
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present on a partial failure, and when the original carried files or voice notes — they are not forwarded and a warning names them (omitted on a clean forward of text only)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`message_not_forwarded` — the caller may not start a new conversation (`error.message` names the organization's rule; see `meta.rules.can_start` on GET /messaging/threads) or group messages are turned off; or `information_barrier` — the company's information barriers keep the caller and a recipient apart"
          },
          "503": {
            "description": "`information_barrier_unavailable` — who is in an information barrier's group couldn't be read just now; try again"
          },
          "404": {
            "description": "Source message or conversation not found"
          },
          "422": {
            "description": "No recipients / recipients unavailable — a recipient who is no longer active is named in `error.message` and listed in `error.details.departed_user_ids`"
          }
        }
      }
    },
    "/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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:messages` scope.\n",
        "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. When it was all the message held (no text, no other file, no voice note) the message itself is deleted instead — to the author's Trash, with the file — and `message_deleted` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message_deleted": {
                      "type": "boolean",
                      "description": "True when removing this file deleted its now-empty message."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the caller's message"
          },
          "404": {
            "description": "Attachment or conversation not found"
          }
        }
      }
    },
    "/messaging/threads/{thread_id}/messages/{message_id}/reactions": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "React to a message (add, idempotent)",
        "description": "Adds the caller's `emoji` reaction to a message in a conversation they\nbelong to (you may react to anyone's message, not only your own). Split\nfrom the web toggle into an explicit add so the outcome does not depend\non current state. Idempotent — re-adding an emoji the caller already left\nreturns 200 with the message unchanged instead of duplicating it. The\nresponse carries the full message with its updated `reactions` block, so\nno follow-up read is needed. `emoji` must be one of the company's reaction\npalette (`GET /messaging/reaction_options` lists it).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "thread_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 company's reaction palette."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction already existed (idempotent repeat)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Reaction added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled / missing write:messages, or `information_barrier` — a new reaction in a one-to-one conversation the company's information barriers keep apart (removing your own still works)"
          },
          "404": {
            "description": "Conversation or message not found"
          },
          "422": {
            "description": "Unsupported emoji"
          }
        }
      }
    },
    "/messaging/threads/{thread_id}/messages/{message_id}/reactions/{id}": {
      "delete": {
        "tags": [
          "Messaging"
        ],
        "summary": "Remove the caller's reaction",
        "description": "Removes the caller's `emoji` reaction. The emoji is passed as the `id`\npath segment (or as an `emoji` query parameter). Idempotent — removing a\nreaction that is not there returns 200 with the message unchanged, so a\nretry does not 404. Only the caller's own reaction is ever affected. The\nresponse carries the full message with its updated `reactions` block.\n",
        "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": "string"
            },
            "description": "The emoji to remove (URL-encoded)."
          },
          {
            "name": "emoji",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Alternative to the path segment."
          }
        ],
        "responses": {
          "200": {
            "description": "Reaction removed (or already absent)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/MessagingMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Messages app disabled / missing write:messages"
          },
          "404": {
            "description": "Conversation or message not found"
          },
          "422": {
            "description": "Unsupported emoji"
          }
        }
      }
    },
    "/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": "q",
            "in": "query",
            "description": "Keep the rooms whose title holds every word typed (a DM's title is\nthe other person's name), or whose active members' names do —\nmember names only in rooms whose member list the caller may see,\nnever the caller's own name; accents ignored. Combines with `scope`\nand pages like the full list. Message content is not searched here\n(that is `GET /chat/search/conversations`).\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "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"
                            },
                            "q": {
                              "type": "string",
                              "nullable": true,
                              "description": "The `q` filter applied (trimmed), or null when none was."
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or insufficient scope"
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Create a room (DM, group or channel)",
        "description": "Creates a direct message (`room_type: direct` + `other_user_id`), a\ngroup (`room_type: group` + `member_user_ids`) or a channel\n(`room_type: channel` + `name`). A DM always answers 200, whether it\nwas made now or already existed (the pair has one DM); 201 is for groups\nand channels only. Gated by the\n`direct_messages_enabled` / `group_chats_enabled` business settings\n(channels ride the group-chats switch).\n\n**Channels** (Chat Wave 1) are made by the same service as the web's\nCreate Channel drawer: only people the \"Who can create channels\"\nsetting allows (403 `channel_not_allowed`; `GET /chat/config` →\n`can_create_channels` says in advance); the name is normalised to\nlowercase words joined by dashes (\"Store 412 Team\" → `store-412-team`)\nand must be unique among the company's channels (422 `name_invalid`).\n\n**Groups** (decision D5) hold up to 20 people, the caller included\n(422 `group_too_large` — `GET /chat/config` → `group_member_limit`), and\nhave no name of their own: they are named after their members. A\n`name` sent with `room_type: group` makes a PRIVATE channel instead when\nthe caller may create channels (`converted_to_channel: true`);\notherwise it is not used and `notice` says so.\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",
                    "description": "A channel's name (required for a channel; normalised, at most 80 characters). For a group, see the description — a group has no name of its own."
                  },
                  "topic": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Channel only — what it is for, shown in Browse Channels."
                  },
                  "avatar": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ChatChannelAvatarChoice"
                      }
                    ],
                    "description": "Channel only — the picture someone chose (as `avatar[emoji]` / `avatar[color]`). Left out, the channel gets the one `GET /chat/channels/avatar_suggestion` previews."
                  },
                  "is_public": {
                    "type": "boolean",
                    "default": true,
                    "description": "Channel only — listed in Browse Channels and open to join (false = private). A JSON boolean or \"true\"/\"false\"."
                  },
                  "posting": {
                    "type": "string",
                    "enum": [
                      "B",
                      "G"
                    ],
                    "default": "B",
                    "description": "Channel only — who may post (B any member, G channel admins only — an announcement channel; members may still reply in threads)."
                  },
                  "member_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Initial members (group/channel; excludes the caller). A group needs at least one."
                  },
                  "recipient_group_id": {
                    "type": "integer",
                    "description": "Channel only, Chat admins only (403 `channel_not_allowed` otherwise) — the channel's members are everyone in this group, kept in step hourly (`member_user_ids` is then not used). Only a group `GET /chat/channels/groups` lists may be followed: groups whose people change on their own (on shift now or working today, near a place, recent and new hires, an attribute match on a dimension the clock moves — tenure, location now, clocked in) and groups that reach nobody until a send names a place or a department (Location Managers, Location Staff) are refused (422 `group_not_found`), and so is a group holding nobody active besides the caller (422 `group_empty`). The response's `members_sync` is `synced` (added now), `queued` (a big group — added within minutes) or `failed` (the group could not be read; the hourly run adds them)."
                  },
                  "photo": {
                    "type": "string",
                    "format": "binary",
                    "description": "Optional group/channel photo."
                  },
                  "photo_signed_id": {
                    "type": "string",
                    "description": "Channel only — an imported list's photo (`photo.signed_id` from `POST /chat/member_imports`)."
                  },
                  "other_member_invite": {
                    "type": "string",
                    "enum": [
                      "Y",
                      "N"
                    ],
                    "description": "Channel only — the room permission codes of ChatRoomPermissionCodes (an imported list's reviewed settings). An `im_permission` sent wins over `posting`; `S` from someone who is not a business admin is 403 `permission_tier_not_allowed`."
                  },
                  "can_view_member": {
                    "type": "string",
                    "enum": [
                      "B",
                      "G"
                    ],
                    "description": "Channel only — see other_member_invite."
                  },
                  "im_permission": {
                    "type": "string",
                    "enum": [
                      "B",
                      "G",
                      "S"
                    ],
                    "description": "Channel only — see other_member_invite."
                  },
                  "important_msg_sender": {
                    "type": "string",
                    "enum": [
                      "B",
                      "G",
                      "S"
                    ],
                    "description": "Channel only — see other_member_invite."
                  },
                  "thread_replies": {
                    "type": "string",
                    "enum": [
                      "B",
                      "G"
                    ],
                    "description": "Channel only — see other_member_invite."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The direct room — made now or already there (a DM always answers 200)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Group or channel created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "converted_to_channel": {
                      "type": "boolean",
                      "description": "room_type=group only — true when the typed name made a private channel (D5)."
                    },
                    "notice": {
                      "type": "string",
                      "nullable": true,
                      "description": "room_type=group only — set when a typed name was not used (\"Groups have no name of their own, so this one shows its members' names.\")."
                    },
                    "photo_error": {
                      "type": "string",
                      "description": "room_type=channel only — present when `photo_signed_id` was sent but the photo couldn't be added (the channel stands); the sentence to show."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled, DMs/group chats disabled, `channel_not_allowed` (may not create channels), `permission_tier_not_allowed` (`S` from someone who is not a business admin), or `information_barrier` (Chat Wave 4 — the company's information barriers keep these people apart; `error.message` is the sentence to show) / 503 `information_barrier_unavailable` (who is in a barrier group couldn't be read just now — try again)"
          },
          "422": {
            "description": "Peer is self / users not in this business / `members_required` / `name_blank` / `name_invalid` (a channel already has it) / `topic_too_long` / `group_too_large` / `invalid_permission_setting` / `chat_not_open` (Chat isn't open to someone picked) / `group_not_found` (`recipient_group_id` is not a group a channel may follow — see `GET /chat/channels/groups`) / `group_empty` (the group holds nobody active besides the caller, so the channel would hold only them)"
          }
        }
      }
    },
    "/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. The caller's read position (and\nthe Seen by others see) moves only for a session or a token holding\nwrite:chat; a read-only token reads without marking. `mark_read=false`\nreads a room the client is not showing (a re-read after `room:updated`,\na preview) without marking it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "mark_read",
            "in": "query",
            "description": "false — return the room and its newest page without marking it read (the read position, unread count and Seen by do not move). Omitted or true — the initial page marks it read, as before.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Return messages strictly older than this id (0 = newest page).",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "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`, and does not mark the room read.\n",
            "schema": {
              "type": "integer"
            }
          },
          {
            "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
            }
          },
          {
            "$ref": "#/components/parameters/ChatThreadsParam"
          }
        ],
        "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
                    },
                    "target_removed": {
                      "type": "boolean",
                      "description": "True when `around_id` names a message that was removed from the room (deleted or taken down by a moderator) — the window shows its neighbours and the client should say the message was removed. False otherwise."
                    }
                  }
                }
              }
            }
          },
          "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",
        "description": "Rename a channel, set its topic, and/or change the photo or settings of\na group/channel. Direct rooms cannot be edited. At least one change is\nrequired.\n\nWho may: a group's admins; a channel's admins and the company's Chat\nadmins (`can_edit_details` on the room) — never while the channel is\narchived (403 `channel_archived`). A channel's name is normalised and\nunique in the company (422 `name_invalid`); a topic change posts a\ntimeline notice.\n\nA group has no name of its own (D5): a NEW `name` for a group answers\n409 `conversion_required` with `conversion_endpoint`\n(`POST /chat/rooms/{id}/convert_to_channel`) and changes nothing. The\ngroup's current name sent back unchanged (an Edit-Group modal saving\nits photo or settings) is ignored and the rest is saved.\n\nThe room's settings ride the same request: any of the five permission\ncodes, with the rules of `PATCH /chat/rooms/{id}/permissions` (keys\nleft out are unchanged; setting or clearing `S` needs a business admin\n— 403 `permission_tier_not_allowed`), and the Ask AI\nproactive-assistance opt-in (`ask_ai_proactive_enabled`,\n`ask_ai_proactive_detectors` — needs the Chat setting\n`ask_ai_enabled`, 403 `ask_ai_disabled`). Every value is checked before\nanything is written, so a refused one leaves no rename applied.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "A channel's new name."
                      },
                      "topic": {
                        "type": "string",
                        "maxLength": 500,
                        "description": "A channel's topic; blank clears it."
                      },
                      "avatar": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ChatChannelAvatarChoice"
                          }
                        ],
                        "nullable": true,
                        "description": "A channel's picture (`avatar[emoji]` / `avatar[color]`); blank picks one from the name again. Refused with 422 `invalid_avatar` before anything is written."
                      },
                      "photo": {
                        "type": "string",
                        "format": "binary",
                        "description": "PNG/JPEG/GIF/WebP, max 10 MB."
                      },
                      "remove_photo": {
                        "type": "boolean",
                        "description": "Clear the existing photo."
                      },
                      "ask_ai_proactive_enabled": {
                        "type": "boolean",
                        "description": "Opt this room in to Ask AI's unprompted suggestions (off by default)."
                      },
                      "ask_ai_proactive_detectors": {
                        "type": "object",
                        "description": "Per-detector switches; detectors left out are unchanged. Any other key, or a value that is not an object, is 422 `invalid_proactive_detectors`.",
                        "properties": {
                          "unanswered_question": {
                            "type": "boolean"
                          },
                          "meeting_intent": {
                            "type": "boolean"
                          },
                          "commitment": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  },
                  {
                    "$ref": "#/components/schemas/ChatRoomPermissionCodes"
                  }
                ]
              }
            }
          }
        },
        "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 / `channel_archived` / `ask_ai_disabled` (the Ask AI keys sent while the Chat setting `ask_ai_enabled` is off) / `permission_tier_not_allowed` (only a business admin may set or clear `S`)"
          },
          "404": {
            "description": "Room not found"
          },
          "409": {
            "description": "`conversion_required` — a new name for a group; make it a private channel instead.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "conversion_endpoint": {
                      "type": "string",
                      "example": "/api/v1/chat/rooms/42/convert_to_channel"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "`direct_room` / `nothing_to_update` / `name_blank` / `name_invalid` / `topic_too_long` / `photo_type` / `photo_too_large` / `photo_not_attached` / `invalid_permission_setting` (a code that setting does not take) / `invalid_proactive_detectors`"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Leave a room (or delete it for everyone with for_all=true)",
        "description": "Default is a per-user soft leave (no-op for DMs). `for_all=true`\ndeletes the room for everyone — room creator or chat app admin\nonly. It is a soft delete: the room and its history can be restored\nfor 30 days from Chat › Recently deleted.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:chat` scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "for_all",
            "in": "query",
            "description": "Delete the room for all members (creator/admin only; restorable for 30 days).",
            "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": "`error.code` says which refusal: `direct_room` — a direct room cannot be deleted for everyone; `legal_hold` — the room is on legal hold (said to a business admin, with where to lift it); `not_deletable` — the same refusal to anyone else, whose message names no hold (the room's `delete_restriction` carries both ahead of time); `not_destroyed` — the delete for everyone did not go through, try again; `creator_cannot_leave` — the room's creator cannot leave it; `last_admin` — the caller is the room's only admin still active in the business, so another member must be made admin first. A leave repeated after the first one ran is not an error: it answers 200 with `left: false`."
          }
        }
      }
    },
    "/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 (or email, where the People directory shows the caller email) for @-mention typeahead. Members on the other side of one of the company's information barriers from the caller are left out (Chat Wave 4).",
        "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": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ChatUserSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "detail": {
                                "type": "string",
                                "description": "What tells this member apart from a colleague with the same name: their job title, then (only for namesakes) their location, department or Employee ID, as far as the People directory shows them to the caller."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "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). A deadline already passed is\nrefused (422 `invalid_muted_until` — epoch seconds sent as milliseconds\nland in 1970 and are refused the same way); one further ahead than 100\nyears is held to 100 years, which is also what \"indefinitely\" stores.\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"
          },
          "422": {
            "description": "`invalid_muted_until` — that deadline has already passed"
          }
        }
      },
      "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"
                    },
                    "read_positions_hidden": {
                      "type": "object",
                      "description": "Present only when read positions were withheld on this page:\nby a works council (a group or channel; never a 1:1\nconversation), or — Chat Wave 4 — for every other member while\nthe company's \"Show who has seen a message\" switch is off\n(reason \"Your company doesn't show who has seen a message.\").\nThose members' `last_read_message_id` is null, which means\n\"not shown\", not \"unread\".\n",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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, or `information_barrier` (Chat Wave 4 — the company's information barriers keep these people apart; `error.message` is the sentence to show) / 503 `information_barrier_unavailable` (who is in a barrier group couldn't be read just now — try again) — someone being added is across a barrier from someone in the room or being added with them"
          },
          "404": {
            "description": "Room not found"
          },
          "409": {
            "description": "`group_full` — a group holds up to `limit` people (D5). Make it a private channel first: POST `conversion_endpoint` with the same `user_ids`, which adds them once it is a channel. Nobody was added.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "example": 20
                    },
                    "user_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "conversion_endpoint": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Direct room / empty user_ids / users not in business / `chat_not_open` (Chat isn't open to someone picked — `error.message` names them) / `archived` (an archived channel takes nobody; restore it first)"
          }
        }
      }
    },
    "/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`; `last_admin` (no other admin still active in this\norganization would remain); `member_inactive` (promoting someone no\nlonger active here); `direct_room` (a 1:1 conversation has no admins).\n"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove a member (self, or any member as room admin)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:chat` scope.\n",
        "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": "Upload a chat attachment",
        "description": "Two legs, and the request decides which.\n\n**Multipart (preferred).** Send the call as `multipart/form-data` with\na `file` part. The bytes go through the API, the content type is\ndetermined from the BYTES (the declared `Content-Type` is not trusted),\nand the upload is finalized in this same request: the response carries\nthe `media` payload with no `upload_url`, and there is nothing further\nto call. Reference the media id in `media_ids` when posting a message.\n`media_type` is optional here — it is derived from the file when\nomitted, and checked against the real bytes when supplied.\n\n**Legacy presign (deprecated).** A JSON body with no file still returns\na presigned PUT URL (15-minute TTL) for native clients already in the\nfield: PUT the bytes, then call `/complete`. It is only available where\nthe deployment's storage supports presigned PUTs; elsewhere it answers\n422 `direct_upload_unsupported`, which means \"send the file as a\nmultipart `file` part instead\".\n\nA file larger than 200 MB belongs on neither leg — open a chunked\nupload session (`POST /uploads/sessions` with `purpose: \"chat\"` and\n`context: { room_id, media_type }`), whose result is the same `media`\npayload.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The attachment itself. Its real content type is sniffed from the bytes."
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "gif",
                      "video",
                      "file"
                    ],
                    "description": "Optional — derived from the file when omitted."
                  },
                  "original_filename": {
                    "type": "string",
                    "description": "Optional — the part's own filename is used when omitted."
                  },
                  "width_px": {
                    "type": "integer",
                    "description": "Images/GIFs only."
                  },
                  "height_px": {
                    "type": "integer",
                    "description": "Images/GIFs only."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_type",
                  "mime_type",
                  "original_filename",
                  "file_size_bytes"
                ],
                "description": "Legacy presign leg — deprecated, see the description.",
                "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": "Multipart: the attachment is stored and finalized (`media` only).\nLegacy presign: the row exists and the bytes are still to be PUT.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/ChatMedia"
                    },
                    "upload_url": {
                      "type": "string",
                      "description": "Legacy presign leg only — absent on the multipart leg."
                    },
                    "storage_key": {
                      "type": "string",
                      "description": "Legacy presign leg only."
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Legacy presign leg only — seconds until upload_url expires."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member",
            "not allowed to send in this room": null,
            "or file uploads disabled": null
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "The file was refused: a type chat does not share (`renderable_mime`,\n`renderable_extension` — HTML, SVG, XML; `blocked_mime`,\n`blocked_extension` — executables; `unsupported_extension`), content\nthat contradicts the extension (`extension_content_mismatch`,\n`mime_type_mismatch`), too large (`file_too_large`), too many files\n(`attachment_cap_reached`), or `direct_upload_unsupported`.\nFor every code above except `direct_upload_unsupported` (which\ntells the client to resend as multipart), `error.message` is\nwritten for the person who picked the file, in their display\nlanguage — show it as the reason instead of a generic \"upload\nfailed\". A renderable or executable EXTENSION is judged before the\ncontent, so the message for one equals that extension's entry in\nGET /chat/config `attachment_rules.refused_extensions`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "renderable_extension"
                        },
                        "message": {
                          "type": "string",
                          "example": "HTML files can't be shared in chat because they can run code when opened. Zip the file or share a PDF instead."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/chat/rooms/{room_id}/media/{id}/complete": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Finalize a legacy presigned upload (uploader only)",
        "description": "LEGACY leg only — call after the presigned PUT succeeds. The multipart\nleg has already done this inside the create call and must not call\nhere. Images/files become `ready` immediately; videos stay `pending`\nuntil transcoding completes. The stored object is measured here, so\nthis can answer 422 (`file_too_large`, `upload_missing`).\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. The caller's read position\n(and the Seen by others see) moves only for a session or a token\nholding write:chat; a read-only token reads without marking, and so\ndoes `mark_read=false`, as on `GET /chat/rooms/{id}`.\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": "mark_read",
            "in": "query",
            "description": "false — read the newest page without marking the room read. Omitted or true — it marks it read, as before.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "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
            }
          },
          {
            "$ref": "#/components/parameters/ChatThreadsParam"
          }
        ],
        "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
                    },
                    "target_removed": {
                      "type": "boolean",
                      "description": "True when `around_id` names a message that was removed from the room (deleted or taken down by a moderator) — the window shows its neighbours and the client should say the message was removed. False otherwise."
                    }
                  }
                }
              }
            }
          },
          "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; allowed in DMs, groups and private channels\nof up to 50 active members — a public or bigger channel answers 403\n`ack_not_allowed_in_channel` (decision D12); important messages\ncannot carry attachments).\n\nChannel threads (D1): a client that draws threads sends a reply in a\nchannel with `in_thread: true` — it is allowed by the room's\n`can_reply_in_thread` (D8: in a channel only admins post in, members\nmay still reply in threads unless a channel admin turned that off) and\nstays in the thread unless `also_send_to_channel` is true AND the\ncaller may post in the channel. A channel reply WITHOUT `in_thread`\n(an older client) posts to the channel as an inline quote, as before.\nA reply to a reply joins its root's thread.\n\nEvery switch on this API (`in_thread`, `also_send_to_channel`, and\n`is_public`, `push_to_talk`, `allow_multiple`, `threads`, `mark_read`\nelsewhere) takes a JSON boolean or the strings \"true\"/\"false\" (\"1\"/\"0\").\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; in a channel, its thread)."
                  },
                  "in_thread": {
                    "type": "boolean",
                    "default": false,
                    "description": "Channel only — this reply belongs to the parent's thread (a client that draws threads)."
                  },
                  "also_send_to_channel": {
                    "type": "boolean",
                    "default": false,
                    "description": "With in_thread — also show the reply in the channel timeline (only for someone who may post there)."
                  },
                  "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)."
                  },
                  "escalate_to_user_id": {
                    "type": "integer",
                    "description": "With `ack_type: important` (a DM, a group or a private channel of up to 50 people): the backup to tell if someone named on the read report has not acknowledged after `escalate_after_minutes`. Any active person Chat is open for other than the sender (they need not be in the room). They get a notice naming who has not acknowledged — the words only if they are in the room; a backup outside the room also gets a \"Message <name>\" link to start a chat with them (decision D14). The sender's own messages then carry `escalation`."
                  },
                  "escalate_after_minutes": {
                    "type": "integer",
                    "enum": [
                      60,
                      120,
                      240,
                      1440
                    ],
                    "description": "How long to wait before telling the backup. Both or neither."
                  }
                }
              }
            }
          }
        },
        "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"
                    },
                    "thread_only": {
                      "type": "boolean",
                      "description": "The reply was kept in its thread",
                      "not the channel timeline.": null
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member / `im_not_allowed` (posting or thread replies not allowed here, or the channel is archived) / `ack_not_allowed_in_channel` (an ack_type in a public channel or one above 50 people — D12) / `important_not_allowed` (Important messages are off for the company, or this room's tier does not include the caller) / `read_receipts_not_allowed` (read-receipt requests are off for the company) / `read_receipts_not_allowed_here` (this room's tier does not include the caller) — the room's `ack_restriction` / `read_receipt_restriction` give the same code and sentence before a send / `information_barrier` (Chat Wave 4 — a direct message the company's information barriers keep apart is read-only; the room's `send_restriction` says so before a send)"
          },
          "422": {
            "description": "Blank body+media / invalid media_ids / disallowed ack_type / bad parent / broadcast_not_allowed (an @channel, @here or group mention from someone Chat settings leave out — Chat Wave 2; ask POST …/mention_audience first) / a refused backup (Chat Wave 3: escalation_incomplete, escalation_needs_important, escalation_not_here, invalid_escalation_wait, invalid_backup, backup_without_chat — `error.message` says why)"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{id}": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "One message",
        "description": "Release 3 · one message of the room, exactly as the history draws it —\n`{ message }`, the same wrapper `PATCH` answers with — so a client can\nredraw one bubble (after a `message:updated`, a push or a link it\nopened) without paging the timeline. Any message the caller may read\nhere: a reply kept in its channel thread too, with `escalation` on the\ncaller's own Important message, and a channel's root message carries its\n`thread` line (as in `GET …/messages?threads=true`).\n\nThe history's own rules: an active member of the room, Chat open to\nthem, and a session or a token with `read:chat` (`read:own_chat` also\nsatisfies, as for the history). Reading it never marks anything read.\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": "The message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a member of the room; `app_not_enabled` / `app_not_accessible` — Chat is not open to the caller; `insufficient_permissions` — the token holds neither `read:chat` nor `read:own_chat`; `work_context_restricted` — the company's Chat access policy (see `error_detail`), as for the history"
          },
          "404": {
            "description": "`room_not_found`; or `message_not_found` — deleted, not there, or in another room"
          }
        }
      },
      "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, and neither is a forward (a message with\n`forwarded_from`): 403 `forwarded_message`, whose `error.message` is\nthe sentence to show (\"Forwarded messages can't be edited. Delete it\nand forward it again.\"). Deleting a forward works as for any message.\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 (`not_editable`) / a forward (`forwarded_message`)"
          },
          "404": {
            "description": "Message not found"
          },
          "422": {
            "description": "Blank body"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Delete a message (author or room admin, soft delete)",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:chat` scope.\n\nThe room hears `message:deleted` (see `POST /chat/pusher/auth`); a\ndeleted thread reply's event carries its root's thread line.\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": "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": "The quick set (GET /chat/reactions) first, but any single emoji is accepted (Chat Wave 2); anything else is 422 unsupported_emoji."
                  }
                }
              }
            }
          }
        },
        "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)",
        "description": "The newest `limit` replies, oldest first; `before_id` pages older ones.\nIn a channel (D1) a reply's id opens its root's thread, `following`\nsays whether the caller follows it (D10), and opening it marks its\nreplies read for them (the Threads view's unread count). In a DM or\ngroup the message must be a root. The caller's read position (and the\nSeen by others see) moves only for a session or a token holding\nwrite:chat; a read-only token reads without marking, and so does\n`mark_read=false`, as on `GET /chat/rooms/{id}`. To mark a thread read\nwithout loading it, `POST …/thread/read`.\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": "mark_read",
            "in": "query",
            "description": "false — read the thread without marking its replies read. Omitted or true — opening it marks them read, as before.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "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"
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "oldest_message_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "following": {
                      "type": "boolean",
                      "description": "Channel threads only; false in a DM or group."
                    },
                    "last_read_reply_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The caller's read position in the thread as it stood before this request marked it read — the last reply they had read. Null when they have none (never opened the thread, or have no follow row). Clients draw a \"New\" line above the first reply past it."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Parent not found or deleted; in a DM or group",
            "a reply": null
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/thread/read": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Mark a channel thread read without loading it",
        "description": "Moves the caller's read position in the thread to its newest reply and\nreads the thread's notices (the \"replied to a thread\" bell row, an\n@mention in a reply) — exactly what opening it with\n`GET …/messages/{message_id}/thread` does, without the replies. A\nreply's id means its root. The Threads view's `unread` and\n`unread_thread_count` drop for a thread the caller follows. A write:\nneeds write:chat, so a read-only token is refused (403) as on\n`PATCH /chat/rooms/{id}/mark_read`.\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": {
          "204": {
            "description": "Marked read (also when there was nothing new)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not a room member",
            "Chat app disabled": null,
            "or a token without write:chat": null
          },
          "404": {
            "description": "`message_not_found` (deleted or not in this room) / room not found"
          },
          "422": {
            "description": "`not_a_channel` — threads are only in channels"
          }
        }
      }
    },
    "/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`). The answer carries\nthe message again (`message`) — its body is no longer masked for the\ncaller, so replace the local row with it. The person's own act: never\nrecorded through an impersonation token or an admin's View-as.\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"
                    },
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member; `attestation_not_delegable` — an impersonation token (only the person themselves can acknowledge); `view_as` — under an admin's View-as"
          },
          "404": {
            "description": "Room or message not found"
          },
          "422": {
            "description": "`ack_not_required` — the 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"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "acknowledged_count": {
                      "type": "integer",
                      "description": "Everyone who acknowledged, named or not — use this, never the list length. Only the named people while `names_hidden.count` is null."
                    },
                    "pending_count": {
                      "type": "integer",
                      "description": "Everyone still to acknowledge, named or not. Only the named people while `names_hidden.count` is null."
                    },
                    "roster_visible": {
                      "type": "boolean",
                      "description": "False when the room's member-visibility setting withholds both name lists (counts still present)."
                    },
                    "can_remind": {
                      "type": "boolean",
                      "description": "True when Remind is available on this message for the caller (the sender). POST …/remind still answers `too_soon` within 24 hours of the last reminder."
                    },
                    "remind_unavailable_reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "not_sender",
                        "direct_chat",
                        "sender_not_eligible",
                        "feature_off",
                        "nobody_pending"
                      ],
                      "description": "Why `can_remind` is false; null when it is true. `sender_not_eligible` means the message was not recorded, when it was sent, as sent by a manager or room admin (a message sent before this server began recording that may lack the record even when its sender was one — see `remind_rule_recorded`)."
                    },
                    "remind_rule_recorded": {
                      "type": "boolean",
                      "description": "False when `sender_not_eligible` says nothing about the sender — a message sent before this server began recording who may be chased (taken as the send time of the earliest chase-eligible message still open — not deleted, and not yet reminded the most times the automatic sweep allows; as those close the date can only move later, which only drops an explanation, never adds a wrong one), or one in a sample-data room. Say why Remind is missing only when this is true."
                    },
                    "names_hidden": {
                      "type": "object",
                      "description": "Present only when a works council hides names (General\nSettings › Countries): the recipients left out of\n`acknowledged` and `pending` — counted, never named.\nWhile the hidden group is under the company's smallest reportable\ngroup, `count` and `group_count` are null and `fewer_than` is that\ngroup, \"a few\" (the status counts beside it count only the named\npeople) — the shape of `BroadcastNamesHidden`.\n",
                      "properties": {
                        "count": {
                          "type": "integer",
                          "nullable": true
                        },
                        "group_count": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null since #4546: a hidden group under the company's smallest group is \"a few\" (`fewer_than`). Kept for older clients."
                        },
                        "fewer_than": {
                          "type": "integer",
                          "description": "Present only when the people it leaves out (or the hidden group) are fewer than the company's smallest group: that number — they are \"a few\", and `count` / `group_count` are null (#4546)."
                        },
                        "reason": {
                          "type": "string",
                          "example": "Names are hidden for people in Germany (works council)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room or message not found"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/follow": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Follow a channel thread (D10)",
        "description": "The caller is notified of new replies. A reply's id means its root.\nThe thread's starter, anyone who replies and anyone @mentioned follow\nautomatically; replying again follows again after an unfollow.\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": "Following",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatThreadFollow"
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`not_a_channel` — threads are only in channels"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Unfollow a channel thread",
        "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": "Not following",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatThreadFollow"
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`not_a_channel`"
          }
        }
      }
    },
    "/chat/rooms/{id}/archive": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Archive a channel (D11)",
        "description": "The channel's admins and the company's Chat admins (who need not be in\nit). An archived channel is read-only, leaves everyone's conversation\nlist and is listed under Archived in `GET /chat/channels`. Delete stays\na separate action. Archiving asks `destructive:chat` (a final act, as\nWikis archive); restoring asks `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not allowed to archive it, or the token lacks `destructive:chat`"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`not_a_channel` / `already_archived` (asked after permission)"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Restore an archived channel",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Restored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not allowed to restore it"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`not_archived`"
          }
        }
      }
    },
    "/chat/rooms/{id}/visibility": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Make a channel public or private (D9)",
        "description": "A channel's admins may make a public channel private; only a company\nChat admin may make a private channel public — warn first that\neverything already said in it becomes readable by anyone who can browse\nchannels. `changed: false` when it already had that visibility.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "is_public"
                ],
                "properties": {
                  "is_public": {
                    "type": "boolean",
                    "description": "A JSON boolean or \"true\"/\"false\"."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Visibility set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "changed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (only a Chat admin may make a channel public)"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`is_public_required` / `not_a_channel` / `archived` (restore it first)"
          }
        }
      }
    },
    "/chat/rooms/{id}/convert_to_channel": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Make a group a private channel (D5)",
        "description": "Where `conversion_required` (naming a group) and `group_full` (adding\npast the limit) send a client. Everyone stays, the history comes along\nand the caller becomes a channel admin. Needs the right to create\nchannels. `user_ids` — the people who were waiting to be added — are\nadded once it is a channel; a refusal to add them is reported in\n`members_error` beside the conversion, which stands.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The channel's name (normalised",
                    "unique in the company).": null
                  },
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Converted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "added": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "rejoined": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "members_error": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`channel_not_allowed` (may not create channels) / `forbidden` (not in the group) / `information_barrier` (Chat Wave 4 — the company's information barriers keep these people apart; `error.message` is the sentence to show) / 503 `information_barrier_unavailable` (who is in a barrier group couldn't be read just now — try again) — checked for `user_ids` before the group changes, so it stays a group"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`not_a_group` / `name_invalid`"
          }
        }
      }
    },
    "/chat/channels": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Browse Channels",
        "description": "Every public channel in the company and the private ones the caller\nis in. Each row is the room shape plus the directory's `member_count`\n(every active member) and `joined`; a channel the caller has not joined\ncarries no `last_message_preview`. 30 a page. 403\n`group_chats_disabled` when group chats are off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Matches name or topic.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "mine",
                "archived"
              ],
              "default": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "members",
                "activity",
                "name",
                "newest"
              ],
              "default": "members"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of channels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channels": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ChatRoom"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "joined": {
                                "type": "boolean"
                              },
                              "join_restriction": {
                                "type": "object",
                                "nullable": true,
                                "description": "Set when POST /chat/channels/{id}/join would refuse it: the channel holds someone the company's information barriers keep the caller from. Draw no Join, and show `message`. Null otherwise.",
                                "properties": {
                                  "code": {
                                    "type": "string",
                                    "enum": [
                                      "information_barrier"
                                    ]
                                  },
                                  "message": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "counts": {
                          "type": "object",
                          "description": "How many channels each filter shows (the filter pills).",
                          "properties": {
                            "all": {
                              "type": "integer"
                            },
                            "mine": {
                              "type": "integer"
                            },
                            "archived": {
                              "type": "integer"
                            }
                          }
                        },
                        "filter": {
                          "type": "string"
                        },
                        "sort": {
                          "type": "string"
                        },
                        "q": {
                          "type": "string",
                          "nullable": true
                        },
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Chat app or group chats disabled"
          }
        }
      }
    },
    "/chat/channels/{id}": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "One public channel",
        "description": "A public channel by id, whether or not the caller is in it — what a\nlink to a channel opens in the app (\"Join #name?\" with its member\ncount, then `POST /chat/channels/{id}/join`). The row is exactly one of\n`GET /chat/channels`' rows. Archived channels are returned too\n(`archived_at` is set; joining one answers 422 `archived`). Anything\nelse — a private channel (whether or not the caller is in it), a\ngroup, a direct message, a deleted room, another company's — answers\n404 `room_not_found`, as `POST …/join` does. 403\n`group_chats_disabled` when group chats are off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The channel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channel": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatRoom"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "member_count": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Every active member; null where the channel's roster is kept to its admins."
                            },
                            "joined": {
                              "type": "boolean"
                            },
                            "join_restriction": {
                              "type": "object",
                              "nullable": true,
                              "description": "Set when POST /chat/channels/{id}/join would refuse it: the channel holds someone the company's information barriers keep the caller from. Draw no Join, and show `message`. Null otherwise.",
                              "properties": {
                                "code": {
                                  "type": "string",
                                  "enum": [
                                    "information_barrier"
                                  ]
                                },
                                "message": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Chat app or group chats disabled"
          },
          "404": {
            "description": "`room_not_found` — not a public channel of this company"
          }
        }
      }
    },
    "/chat/channels/similar": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Channels like the one being named",
        "description": "Up to three channels the caller can see whose name or topic shares a\nword (three letters or more) with `q` — shown while naming a new\nchannel so a duplicate is not made by accident.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Similar channels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "topic": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_public": {
                            "type": "boolean"
                          },
                          "joined": {
                            "type": "boolean",
                            "description": "The caller is in it — offer Open; otherwise offer Browse channels on it (`GET /chat/channels?q=<name>`), where Join is."
                          },
                          "member_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Null where the channel's roster is kept to its admins."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/chat/channels/avatar_suggestion": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The picture a new channel with this name would get",
        "description": "The emoji and colour `POST /chat/rooms` stores for a channel created\nwith this `name` and `topic` and no `avatar` — so a create dialog can\npreview the picture as it is typed — and the emoji its Shuffle offers.\nWrites nothing. A name that normalises to nothing answers\n`{ avatar: null, emoji_choices: [] }`. Only for people who may create\nchannels (`GET /chat/config` → `can_create_channels`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "The channel name being typed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "topic",
            "in": "query",
            "required": false,
            "description": "The topic being typed; its words help pick the emoji.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The suggested picture",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "avatar": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "emoji": {
                          "type": "string",
                          "nullable": true,
                          "example": "📣"
                        },
                        "color": {
                          "$ref": "#/components/schemas/ChatChannelAvatarColor"
                        }
                      }
                    },
                    "emoji_choices": {
                      "type": "array",
                      "maxItems": 8,
                      "description": "Up to eight emoji to offer, the best match first.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "📣",
                        "📢",
                        "💬"
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`not_allowed` — may not create channels; `group_chats_disabled` — channels are turned off for the organization"
          }
        }
      }
    },
    "/chat/channels/groups": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Groups a new channel may follow",
        "description": "Chat Wave 3 — the Create a channel form's Members → \"Everyone in a\ngroup\" list: the company's own groups a channel's members may come\nfrom, by name, whose name contains `q`. Never one whose people change\non their own (on shift now or working today, near a place, recent and\nnew hires, an attribute match on a dimension the clock moves — tenure,\nlocation now, clocked in), and never one that reaches nobody until a\nsend names a place or a department (Location Managers, Location\nStaff); `POST /chat/rooms` refuses those with 422 `group_not_found`.\nThe first 50; `has_more`\nsays to narrow with `q`. Pass a group's `id` as `recipient_group_id`\non `POST /chat/rooms` (`room_type: channel`). Only for people\n`GET /chat/config` → `can_create_synced_channels` is true for (a\nbusiness admin or Chat admin who may create channels, under a token\nwith `write:chat`) — anyone else gets 403 `channel_not_allowed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Part of a group's name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Groups by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "type_label": {
                            "type": "string",
                            "description": "What kind of group it is, in words (NotificationRecipientGroup#type_label) — e.g. Static, Attribute match."
                          }
                        }
                      }
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "More groups match than were listed — narrow with `q`."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`channel_not_allowed` (may not make a channel whose members come from a group; `error.message` is the sentence) / `group_chats_disabled` / Chat app disabled"
          }
        }
      }
    },
    "/chat/channels/{id}/join": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Join a public channel",
        "description": "A person who left comes back. `joined: false` when the caller was\nalready a member (nothing changed). A private channel answers 404, as\nfor a room that is not there.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Joined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "joined": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`group_chats_disabled` (channels are off for the company) / `forbidden` (not an active person here) / `information_barrier` (Chat Wave 4 — the company's information barriers keep these people apart; `error.message` is the sentence to show) / 503 `information_barrier_unavailable` (who is in a barrier group couldn't be read just now — try again) — the channel holds someone the caller may not message"
          },
          "404": {
            "description": "Not a public channel in this company"
          },
          "422": {
            "description": "`archived`"
          }
        }
      }
    },
    "/chat/threads": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The Threads view — threads the caller follows (D10)",
        "description": "Channel threads the caller follows that have replies, in rooms they\nare still in, newest reply first, with `unread_thread_count` (the badge\non \"Threads\") and `total_count` (every thread this list holds, not one\npage). Each row previews its newest replies (`latest_replies`, up to 2,\noldest first). Listing never marks anything read; opening a thread\n(`GET …/messages/{id}/thread`) or `POST …/messages/{id}/thread/read`\ndoes.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "`next_before` of the previous page",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Followed threads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "threads": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "root": {
                            "$ref": "#/components/schemas/ChatMessage"
                          },
                          "room": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "room_type": {
                                "type": "string"
                              },
                              "is_public": {
                                "type": "boolean"
                              }
                            }
                          },
                          "reply_count": {
                            "type": "integer"
                          },
                          "unread": {
                            "type": "integer"
                          },
                          "last_reply_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "repliers": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/ChatUserSummary"
                            }
                          },
                          "latest_replies": {
                            "type": "array",
                            "description": "The thread's newest replies (at most 2), oldest first; deleted ones left out.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "integer"
                                },
                                "user_id": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "user_name": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "user_avatar_url": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "user_avatar_updated_at": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "Epoch seconds the author's avatar last changed (cache-bust)."
                                },
                                "body": {
                                  "type": "string",
                                  "description": "As a ChatMessage's `body` — the read-receipt notice while `body_masked`; \"\" for an attachment-only reply."
                                },
                                "body_masked": {
                                  "type": "boolean",
                                  "description": "A read-receipt reply the caller has not acknowledged yet."
                                },
                                "has_attachments": {
                                  "type": "boolean"
                                },
                                "is_system": {
                                  "type": "boolean",
                                  "description": "A server notice — draw it as a ChatMessage's `is_system`."
                                },
                                "author_is_ai": {
                                  "type": "boolean",
                                  "description": "Written by Ask AI — as a ChatMessage's `author_is_ai`."
                                },
                                "created_at": {
                                  "type": "string",
                                  "format": "date-time"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "unread_thread_count": {
                      "type": "integer"
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Threads the caller follows that this list holds (have replies, in rooms they can open) — all pages."
                    },
                    "next_before": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Cursor for the next page; null on the last page"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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.\n\nDecision D5: a group holds up to 20 people, the caller included (422\n`group_too_large`), and has no name of its own — it is named after its\nmembers. A typed `name` on a NEW conversation makes it a PRIVATE channel\nwhen the caller may create channels (`converted_to_channel: true`) and\nis otherwise not used (`notice` says so). The same rule as the web's\nStart a Chat and the phone's picker (Apps::Chat::StartChatService).\n\nThe Create-a-Group modal's settings may ride at the top level of the\nbody — the five permission codes of `PATCH /chat/rooms/{id}/permissions`;\nthose left out take their defaults. They are written only on a group\n(or private channel) this call makes — not on a DM, nor on an existing\nconversation handed back. They are checked before anything is made: a\ncode a setting does not take is 422\n`invalid_permission_setting`, and `S` from someone who is not a\nbusiness admin is 403 `permission_tier_not_allowed`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "required": [
                      "selected_user_ids"
                    ],
                    "properties": {
                      "selected_user_ids": {
                        "type": "array",
                        "items": {
                          "type": "integer"
                        }
                      },
                      "name": {
                        "type": "string",
                        "description": "Optional — makes a new conversation a private channel when the caller may create channels (ignored for DMs)."
                      },
                      "create_new_conv_always": {
                        "type": "boolean",
                        "description": "Skip dedup and always create a new room."
                      },
                      "photo_signed_id": {
                        "type": "string",
                        "description": "An imported list's photo (`photo.signed_id` from `POST /chat/member_imports`), set on a group or private channel this call creates."
                      }
                    }
                  },
                  {
                    "$ref": "#/components/schemas/ChatRoomPermissionCodes"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room found or created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    },
                    "created": {
                      "type": "boolean"
                    },
                    "converted_to_channel": {
                      "type": "boolean",
                      "description": "A typed name made a new private channel."
                    },
                    "notice": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when a typed name was not used for a new group."
                    },
                    "photo_error": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when `photo_signed_id` was sent but not used — it couldn't be added, the conversation already existed, or it is a direct message (the conversation stands either way). The sentence to show."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or DMs/group chats disabled, or `information_barrier` (Chat Wave 4 — the company's information barriers keep these people apart; `error.message` is the sentence to show) / 503 `information_barrier_unavailable` (who is in a barrier group couldn't be read just now — try again) — the caller and a picked person, or two picked people, are on opposite sides of a barrier (refused whether or not the conversation already exists); `permission_tier_not_allowed` — only a business admin may set `S`"
          },
          "422": {
            "description": "No users selected / self-only / users not in business / `chat_not_open` (Chat isn't open to someone picked — `error.message` names them) / `group_too_large` / `name_invalid` (a channel already has that name) / `invalid_permission_setting` (a code that setting does not take)"
          }
        }
      }
    },
    "/chat/member_imports": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Import a member list — read it and match it to the company's members",
        "description": "The review behind \"Import a list\" in New message, Create a channel and\nAdd members (web, phone and desktop share it). Send any list the\nperson has as `content` — pasted text, or a file the client read as\ntext (CSV, TSV, JSON, vCard, an email's recipients, copied spreadsheet\ncells…), up to 2 MB. One AI call reads the people and what the list\nsays about itself (its name, topic, public or private, photo and\nsettings); the matching is deterministic, on the server: email (only\nwhere the caller's directory shows email), employee ID, exact name,\nthen close names offered as \"did you mean\". Only the caller's own\ncontent is sent to the AI, never the directory.\n\nEach row of `rows` is `ready` (one member, ticked), `pick` (several\nmembers share the name or address, or only close names — choose one\nof `candidates`, none is chosen for the person), `not_found`, or\n`unavailable` with `reason_code` — `you`, `already_member`,\n`deactivated`, `chat_not_open` (Chat isn't open to them),\n`information_barrier`, `barrier_room` (kept apart from someone in the\nconversation), `barrier_list` (kept apart from someone earlier on the\nlist) — and `reason`, the sentence to show in the caller's language.\n\nNothing is created. The client creates with the people confirmed\nthrough `POST /chat/start_chat` (a group, or a private channel past\n`conversation.group_member_limit` or with a name), `POST /chat/rooms`\n(`room_type: channel`) or `POST /chat/rooms/{id}/members`, passing the\nreviewed `settings.values` as the five permission codes and\n`photo.signed_id` as `photo_signed_id` (kept for a day). Settings and\nthe photo are not reviewed for an existing conversation (`room_id`).\n\nReading `content` spends AI credits and counts against an hourly limit\nshared with the web and phone (429 `rate_limited`); matching `people`\nthe client already read, or copying a conversation's people\n(`from_room_id`), does not.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "The list as text, at most 2 MB. One of this, `people` or `from_room_id`."
                  },
                  "people": {
                    "type": "array",
                    "description": "People already read (matched with no AI call). One of this, `content` or `from_room_id`. The first 500 are read (`truncated` says when there were more); each value is cut to 300 characters.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "employee_id": {
                          "type": "string"
                        },
                        "job_title": {
                          "type": "string"
                        },
                        "department": {
                          "type": "string"
                        },
                        "location": {
                          "type": "string"
                        },
                        "list": {
                          "type": "string",
                          "description": "Which of several lists the person is on."
                        }
                      }
                    }
                  },
                  "from_room_id": {
                    "type": "integer",
                    "description": "Instead of `content` or `people`: everyone in this group or channel (\"Add everyone from a channel\"), sent to the review with no AI call and no hourly limit — the person copying left out, each row `match: conversation`, and the review's `list.copied_from` names it. Only a group or channel the person is in and whose members they may see; GET /chat/member_imports/sources lists them. The first 500 are copied (`truncated` says when there were more)."
                  },
                  "room_id": {
                    "type": "integer",
                    "description": "Adding to this group or channel (Add members). Omit for a new conversation."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The review",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatMemberImport"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` (not in that conversation) / `not_allowed` (may not add people to it) / `ai_unavailable` (AI is off for Chat for this person) / `source_hidden` (`from_room_id` names a conversation the person is in whose members they may not see)"
          },
          "404": {
            "description": "`room_not_found` (`room_id` names no conversation here, or `from_room_id` none the person is in — the same answer whatever the conversation is)"
          },
          "422": {
            "description": "`invalid_room_type` (a direct message) / `invalid_source` (`from_room_id` is not a group or channel) / `org_synced` (its members come from a group, location, team or shift — `error.synced_from`) / `ai_usage_blocked` (the AI budget is paused or spent; `error.message` is the sentence) / `no_people` (nothing in it reads as a person) / `blank` / `content_too_large` / `too_long` / `too_many` (more than one read can take — split it) / `ai_failed`"
          },
          "429": {
            "description": "`rate_limited` — the hourly limit; `error.message` says when it lifts. Counted only for a read of `content` that passed every other check."
          }
        }
      }
    },
    "/chat/member_imports/sources": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The groups and channels whose people the caller may copy",
        "description": "For \"Add everyone from a channel\": the caller's groups and channels\nwhose members they may see (the room's \"Who can see members?\", with\nthe token's own limits on acting as a company admin) with someone else\nin them, most recently active first, at most 200 — `truncated` says\nmore qualify. Pass a room's `id` as `from_room_id` to POST\n/chat/member_imports.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "The conversation people are being added to — left out of the list."
          }
        ],
        "responses": {
          "200": {
            "description": "The rooms",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rooms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "group",
                              "channel"
                            ]
                          },
                          "title": {
                            "type": "string",
                            "description": "A channel's \"#name\", or a group's title as the conversation list names it."
                          },
                          "others": {
                            "type": "integer",
                            "description": "How many people besides the caller are in it."
                          }
                        }
                      }
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "More rooms qualify than the 200 listed (the most recently active are)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled",
            "or token lacks read:chat": null
          },
          "404": {
            "description": "`room_not_found` (`room_id` names no conversation here)"
          }
        }
      }
    },
    "/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.\nPeople on the other side of one of the company's information barriers\nfrom the caller are not offered (Chat Wave 4 — starting a chat with\nthem answers 403 `information_barrier`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active_only",
            "in": "query",
            "description": "`false` also returns people who have left the business (deactivated\nhere, or whose account is off). Honoured ONLY for a caller who is an\nadmin or above in the business; for anyone else it is ignored and\nthe active directory is returned.\n",
            "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": "thread_id",
            "in": "query",
            "description": "A Messages direct-message thread (Inbox → Messages, not a Chat room)\nthe caller is in: results are limited to its participants. A thread\nthat is not the caller's, or does not exist, returns no rows. Chat's\ninformation-barrier filter does not apply here — the participants\nare that app's.\n",
            "schema": {
              "type": "integer"
            }
          },
          {
            "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"
            }
          },
          {
            "name": "chat_users",
            "in": "query",
            "description": "Release 3 · `1` (or `true`): only people Chat is open to — the web\npickers' `chat_users=1`, the rule a backup for an Important message\nis refused on (`backup_without_chat`). Pick a backup with it.\n",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "exclude_ids",
            "in": "query",
            "description": "Release 3 · user ids never to offer (the room's members, people\nalready picked), comma-separated: `exclude_ids=4,9`\n(`exclude_ids[]=4&exclude_ids[]=9` works too, for a few). At most\n1,000 (422 `too_many_exclude_ids` past it) — send a long list\ncomma-separated: the server refuses a query string over 10 KB\nbefore it is read. A narrowing only — every door still checks whom\nit adds.\n",
            "schema": {
              "type": "string",
              "example": "4,9"
            }
          }
        ],
        "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",
                            "nullable": true,
                            "description": "Null where the People directory does not show the caller email (and for someone with none)."
                          },
                          "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"
          },
          "422": {
            "description": "`too_many_exclude_ids` — more than 1,000 ids in `exclude_ids`"
          }
        }
      }
    },
    "/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. Each row names its\nconversation (`room_name`, `room_type`), says whether it is `unread`\n— past the caller's read position in the room, or in the thread for a\nreply kept there; in a room they are still in that is not archived —\nand, for a mention in a channel thread reply, the thread to open it in\n(`thread_root_id`) and whether the reply stayed there (`thread_only`).\n`unread_count` is the list's unread mentions (one room's with\n`room_id`), the same count as the rooms' `unread_mention_count`.\nReading this list marks nothing read.\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"
                          },
                          "room_name": {
                            "type": "string",
                            "nullable": true,
                            "description": "The conversation's title as the conversation list draws it — the room payload's `display_name`, in the caller's language: a DM by the other person's name, a group or channel by its own, a group with none by its people where the caller may see who is in it (\"Ann, Bo, Cy and 2 more\"), else \"Untitled group\". Null only for a mention whose message is gone."
                          },
                          "room_type": {
                            "type": "string",
                            "enum": [
                              "direct",
                              "group",
                              "channel"
                            ],
                            "nullable": true
                          },
                          "unread": {
                            "type": "boolean",
                            "description": "Not read yet (the rule behind the rooms' `unread_mention_count`). Always false in a room the caller has left or an archived channel."
                          },
                          "thread_root_id": {
                            "type": "integer",
                            "nullable": true,
                            "description": "For a mention in a channel thread reply, the thread's first message — open `GET /chat/rooms/{room_id}/messages/{thread_root_id}/thread`. Null otherwise (a DM or group quote reply included)."
                          },
                          "thread_only": {
                            "type": "boolean",
                            "description": "The thread reply was kept in its thread (not also sent to the channel timeline). False when `thread_root_id` is null."
                          },
                          "room_accessible": {
                            "type": "boolean",
                            "description": "False when the caller has since left that room (or its message is gone) — the mention is kept as history, but the room endpoints refuse it. Show the row; do not link it."
                          },
                          "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
                    },
                    "unread_count": {
                      "type": "integer",
                      "description": "Unread mentions in this list — all pages; one room's with `room_id`."
                    }
                  }
                }
              }
            }
          },
          "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 (or email, where the People directory shows the caller 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.\nNot paged: `has_more` says more than 50 conversations matched, and\n`content_truncated` that content matching stopped at its 50 rooms.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_people",
            "in": "query",
            "description": "`true` or `1`: the answer also carries `people` — the first page of\n`GET /chat/users/search` for the same `q` (default filters), so a\nunified search panel needs one request. Empty below 2 characters.\n",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations matching by name or message content",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "maxItems": 50,
                      "items": {
                        "$ref": "#/components/schemas/ChatRoom"
                      }
                    },
                    "content_truncated": {
                      "type": "boolean",
                      "description": "Message-content matching looked only at the 50 rooms whose newest match is newest, so an older match elsewhere may be missing — say so rather than \"no conversation mentions it\"."
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "More than the 50 conversations returned matched; narrow `q`."
                    },
                    "people": {
                      "type": "array",
                      "description": "Only with `include_people` — rows shaped like `GET /chat/users/search` `results`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "text": {
                            "type": "string"
                          },
                          "email": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar_url": {
                            "type": "string"
                          },
                          "meta": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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."
                    },
                    "read_only_keys": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Settings in `settings` this caller may read but not change — a Chat app admin who is not a company admin gets the four company-admin keys (PATCH refuses them with 403 `company_admin_only`); empty otherwise."
                    },
                    "retention": {
                      "type": "object",
                      "description": "How long each kind of conversation keeps its messages, in days (30–3650); null = forever (the default). Every night, messages older than this are hidden from everyone, and permanently deleted 30 days later; anything under legal hold is kept for the company's records (people no longer see it). A room's own `retention_days` says the same for that room. Set by a company admin on the web Chat settings page only; PATCH here does not write it.",
                      "properties": {
                        "direct": {
                          "type": "integer",
                          "nullable": true,
                          "minimum": 30,
                          "maximum": 3650
                        },
                        "group": {
                          "type": "integer",
                          "nullable": true,
                          "minimum": 30,
                          "maximum": 3650
                        },
                        "channel": {
                          "type": "integer",
                          "nullable": true,
                          "minimum": 30,
                          "maximum": 3650
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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. `content_moderation_enabled`, `ai_moderation_enabled`, `moderation_action` and `chat_video_subtitles_enabled` are written only by a company admin; a Chat app admin who sends any of them with a CHANGED value gets 403 `company_admin_only` (the keys in `error.keys`) and nothing is saved. Sent back unchanged, they are ignored.",
        "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 connected; clients call this periodically. `state`\nsays whether they are active (`online`, the default) or away from the\nkeyboard (`idle` — peers draw the amber dot); `away` is read as `idle`\nand anything else as `online`.\n\nThe OneMango Desktop app also sends `in_use_for`: how many more\nseconds it counts as in use (5 minutes after the last keyboard or\nmouse activity, 0 while the screen is locked). While it is in use, a\ncompany with \"One interruption per item\" on shows each notification\nthere as a desktop pop-up instead of pushing the phone.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "Also accepted in the body.",
            "schema": {
              "type": "string",
              "enum": [
                "online",
                "idle"
              ],
              "default": "online"
            }
          },
          {
            "name": "in_use_for",
            "in": "query",
            "description": "Desktop app only (read from an Electron request). Seconds the app\nstays in use, 0–300; 0 says it is not in use now. Also accepted in\nthe body.\n",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 300
            }
          },
          {
            "name": "device",
            "in": "query",
            "description": "Desktop app only. Which desktop is beating (an id for this run of\nthe app), so one computer locking never clears another the person\nis using. Also accepted in the body.\n",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "online",
                        "idle"
                      ],
                      "description": "The state recorded."
                    }
                  }
                }
              }
            }
          },
          "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, and may see\nthe room's member list) or an explicit `user_ids` list\n(comma-separated, max 200). Room mode reads the 200 longest-standing\nmembers; `truncated` says there are more — ask about the rest in\n`user_ids` batches.\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",
                      "description": "Connected — an idle user is in it.",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "presence_states": {
                      "type": "object",
                      "description": "User id (as a string) → `online` or `idle`; an id left out is offline.",
                      "additionalProperties": {
                        "type": "string",
                        "enum": [
                          "online",
                          "idle"
                        ]
                      },
                      "example": {
                        "42": "online",
                        "57": "idle"
                      }
                    },
                    "room_id": {
                      "type": "integer",
                      "description": "Echoed only in room_id mode."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "Room mode only — the room has more than 200 active members",
                      "and only the first 200 were read.": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a member of the room, or (room mode) the room's member list is kept to admins; or Chat app disabled"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`too_many_user_ids` — 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>`,\n`presence-room-<id>` (presence responses also include `channel_data`),\nand `private-business-<business_id>-chat-settings` (`settings:updated`\n— the company's Chat settings changed; any member of that business).\n\n`room:updated` on `private-room-<id>` (`{ room_id }`): a channel's or\ngroup's name, topic, photo, visibility (`is_public`), archived state,\nkind (a group turned into a channel), settings (who can post, add\nmembers, see members, send Important messages; who may reply in\nthreads; the Ask AI suggestions switch) or the source its members\nfollow changed — from any door (the web, this API, the phone, Ask AI,\nthe company's org syncs). Re-read the room with `GET /chat/rooms/{id}?mark_read=false`;\nthe payload carries no state because the room's `can_*` flags and\nrestrictions differ per person. Sent on the room channel only, never\nto inbox channels — a list with the room closed picks the change up on\nits next fetch.\n\n`reminder:due` on the caller's own inbox channel\n(`{ reminder_id, room_id, message_id, room_name, excerpt }`): a reminder\nthey set (`POST …/messages/{message_id}/reminder`) is due and its bell\nrow was written — see that endpoint.\n\n`status:changed` on the caller's own inbox channel (`{ user_id }`):\ntheir status or pause changed from any door (the web, the phone, this\nAPI, Ask AI) — re-read `GET /chat/status`. Sent to the person's own\nchannel only, never for a no-op save, nor when a status or pause runs\nout (time those from `expires_at` / `dnd_until`).\n\n`scheduled_messages:changed` on the caller's own inbox channel\n(`{ room_id, scheduled_message_id, state }`, Release 3): one of their\nscheduled messages changed — `state` is `created`, `updated` (its\nwords or time changed, or a failed one was rescheduled), `sent`,\n`failed` (its `error` says why — also when the message a scheduled\nreply answered was permanently deleted: it stays as \"Not sent\", no\nlonger a reply) or `cancelled` — from any door (the\nweb, the phone, this API, Ask AI) or the minute job that sends what is\ndue. Re-read `GET /chat/rooms/{room_id}/scheduled_messages` (and the\nroom's \"N scheduled messages\" line). Not sent while a message is being\nsent (`sending`), and only to its author; a sent one also arrives as an\nordinary `message:created`.\n\n`message:created` (room and inbox channels) carries `permalink`, the\nsame absolute link as the message's `permalink` in REST answers, and\n`forwarded_from`, the same object as the REST message's (null when the\nmessage is not a forward; on an event marked `body_omitted: true` it\nmay be null too when the quote alone was too long — re-read the\nmessage then).\n\n`message:deleted` on `private-room-<id>` — a message was deleted (by\nits author, a room admin, or moderation):\n`{ id, room_id, parent_message_id, in_thread, parent_thread }`.\n`parent_message_id` and `in_thread` are the deleted message's own\n(the same keys as a `ChatMessage`; `parent_message_id` is null for a\ntop-level message). With `in_thread` (a reply in a channel thread),\n`parent_thread` is the ROOT's thread line as it stands after the\ndelete — `{ reply_count, last_reply_at, repliers }`, the same keys and\ncount as the root's `thread` in `GET …/messages?threads=true`, without\nthe per-viewer `following`: not-deleted replies, whether kept in the\nthread or also sent to the channel. Update the root's \"N replies\" /\nlast-reply line from it even when the thread is closed and the reply\nis not on screen; `reply_count` 0 means nobody's reply is left.\n`parent_thread` is null when `in_thread` is false.\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) — the company's quick set, not reactions on a message.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "emoji": {
                            "type": "string",
                            "example": "👍"
                          },
                          "key": {
                            "type": "string",
                            "nullable": true,
                            "example": "like",
                            "description": "The reaction's catalog key; null for a glyph outside the catalog."
                          },
                          "label": {
                            "type": "string",
                            "example": "Like",
                            "description": "For tooltips and screen readers."
                          },
                          "icon_url": {
                            "type": "string",
                            "nullable": true,
                            "example": "https://acme.workforce.mangoapps.com/assets/reactions/like-1a2b3c.svg",
                            "description": "Absolute URL of the drawn icon the web shows for this reaction. null ⇒ draw the emoji glyph. Draw the glyph too when the image does not load."
                          }
                        }
                      }
                    },
                    "reaction_icons": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "👍": "https://acme.workforce.mangoapps.com/assets/reactions/like-1a2b3c.svg",
                        "😂": "https://acme.workforce.mangoapps.com/assets/reactions/haha-4d5e6f.svg"
                      },
                      "description": "Every emoji the platform draws as an icon → that icon's absolute URL —\nwider than `reactions`: a message keeps reactions the palette no longer\noffers, older glyphs that mean the same reaction (😂 is Haha, ❤ without\nVS16 is Love) and emoji sent from the full picker. Draw a message's\nreaction from here; an emoji that is not a key has no icon and is drawn\nas its glyph.\n"
                    },
                    "reactions_accept_any_emoji": {
                      "type": "boolean",
                      "description": "Chat Wave 2: `reactions` is the quick set; POST …/reactions also takes any single emoji, so a client may offer a full emoji picker behind \"More\"."
                    },
                    "klipy": {
                      "type": "object",
                      "description": "GIF-picker (KLIPY) bootstrap. enabled=false ⇒ hide the picker. New clients search through GET /chat/gifs and need no key; `api_key` stays for the clients that still search KLIPY directly.",
                      "properties": {
                        "api_key": {
                          "type": "string",
                          "nullable": true
                        },
                        "base_url": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        }
                      }
                    },
                    "attachment_rules": {
                      "type": "object",
                      "description": "Which file extensions an attachment may carry, so a client can refuse a file at pick time instead of uploading it to a 422. Advisory — POST /chat/rooms/{room_id}/media enforces every rule (and judges the real bytes). Match on the lowercased extension with its dot. Sentences are in the caller's display language; show them as written.",
                      "properties": {
                        "allowed_extensions": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "example": ".pdf"
                          },
                          "description": "Every extension chat accepts. A file with no extension is also accepted."
                        },
                        "refused_extensions": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "Extension → the sentence to show for it, e.g. \".html\" → \"HTML files can't be shared in chat because they can run code when opened. Zip the file or share a PDF instead.\""
                        },
                        "unsupported_extension_message": {
                          "type": "string",
                          "description": "The sentence for any other extension not in allowed_extensions — replace %{ext} with the file's extension."
                        }
                      }
                    },
                    "can_create_channels": {
                      "type": "boolean",
                      "description": "Whether the caller may create a channel (\"Who can create channels\"), on a tenant with group chats on and a credential that may write. Show Create Channel only when true."
                    },
                    "can_create_synced_channels": {
                      "type": "boolean",
                      "description": "Whether the caller may make a channel whose members are everyone in a group (Create a channel → Members → \"Everyone in a group\"): a business admin or Chat admin who may create channels, under a token with `write:chat` (not only `write:own_chat`). Offer that choice only when true; list the groups with GET /chat/channels/groups and send the pick as `recipient_group_id` on POST /chat/rooms."
                    },
                    "group_member_limit": {
                      "type": "integer",
                      "example": 20,
                      "description": "A group holds up to this many people, the creator included (D5)."
                    },
                    "member_import_available": {
                      "type": "boolean",
                      "description": "Whether to offer \"Import a list\" (POST /chat/member_imports) in New message, Create a channel and Add members: AI is on for Chat, the caller's AI usage is not blocked, and the token may write. Draw the link only when true."
                    },
                    "member_copy_available": {
                      "type": "boolean",
                      "description": "Whether to offer \"Add everyone from a channel\" (the rooms from GET /chat/member_imports/sources, then POST /chat/member_imports with from_room_id): no AI, so true whenever the token may write — where member_import_available is false, offer this choice alone."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/ask_ai/identity": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The Ask AI assistant's identity (name, icon, description, greeting, history tip)",
        "description": "Any chat member. Answered when the Ask AI app is on for the business (the persona is an Ask AI-wide setting) or Chat's \"@Ask AI in chat\" toggle is on. A read only — it never creates the assistant's account. Before that account exists the answer is the default identity: `id` null, the default name, `avatar_url`, `avatar_updated_at`, `description` and `history_tip` null, and the persona's greeting.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ask_ai_identity": {
                      "$ref": "#/components/schemas/ChatAskAiIdentity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "Chat app disabled, or Ask AI not enabled (error.code \"not_available\")"
          }
        }
      },
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Update the Ask AI assistant's identity (admin only)",
        "description": "Send any subset of fields as multipart/form-data; omitted fields are unchanged and an empty string clears a text field (except `name`, which cannot be blank). The PERSONA — name, description, greeting, avatar — is the same setting as Ask AI › Guidelines › Assistant: it needs an organization admin and the Ask AI app on. `history_tip` is Chat-only: an organization admin or a Chat app admin, with Chat's \"@Ask AI in chat\" toggle on.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Letters and numbers in any script, spaces and . ' ’ & -, starting with a letter or number; not the name of a person in the organization. The assistant introduces itself by it in every answer."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Subtitle shown under the name in Chat."
                  },
                  "greeting": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "The assistant's opening line; empty ⇒ the built-in welcome."
                  },
                  "history_tip": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "One-time tip on the first history-grounded answer in a Chat room; empty ⇒ the built-in tip."
                  },
                  "avatar": {
                    "type": "string",
                    "format": "binary",
                    "description": "Icon — PNG, JPG, GIF or WebP, under 5 MB. An empty part leaves the icon unchanged."
                  },
                  "remove_avatar": {
                    "type": "boolean",
                    "description": "Clear the custom icon (a persona change — the same organization-admin rule)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ask_ai_identity": {
                      "$ref": "#/components/schemas/ChatAskAiIdentity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "Not permitted for these fields (error.code \"forbidden\"), Ask AI / the Chat toggle not enabled (error.code \"not_available\"), or Ask AI is not available to this business (error.code \"no_account\" / \"missing_business\")"
          },
          "409": {
            "description": "Another account in the business already uses that name (error.code \"name_taken\")"
          },
          "422": {
            "description": "Invalid name, text too long, or an icon of the wrong type or size"
          },
          "500": {
            "description": "The identity could not be saved (error.code \"update_failed\") — try again"
          }
        }
      }
    },
    "/chat/gifs": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Search GIFs (KLIPY) through the server",
        "description": "The web composer's GIF search, for a client that holds no KLIPY key:\nthe same content filter and the same per-person id KLIPY is sent.\nTrending when `q` is blank. Pick one with POST\n/chat/rooms/{room_id}/media/gif, sending the GIF's `slug` and the `q`\nit was found with. A picked GIF is a file in the room, so the\norganization's file-sharing switch applies here too.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The search; blank for trending."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of GIFs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "gifs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string",
                            "description": "What POST …/media/gif takes."
                          },
                          "key": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "preview_url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "width": {
                            "type": "integer",
                            "nullable": true
                          },
                          "height": {
                            "type": "integer",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "has_next": {
                      "type": "boolean"
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "klipy"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in"
          },
          "403": {
            "description": "file_uploads_disabled — sharing files is off for the organization; or Chat is not open to the caller (`read:chat` required)."
          },
          "502": {
            "description": "gif_unavailable — KLIPY did not answer."
          },
          "503": {
            "description": "gif_not_configured — this deployment has no KLIPY key."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/media/gif": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Add a GIF picked from KLIPY as an attachment",
        "description": "The server looks the KLIPY `slug` up (GET /chat/gifs is how a client\nsearches without a key), downloads the rendition under\nchat's attachment size cap and stores it like an upload — the same\n`media` answer as POST …/media. Send its id in `media_ids` with the next\nmessage. Same gates as an upload: file uploads on, a member who may\npost or reply in a thread here, the per-room attachment cap.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "The slug KLIPY's search answered",
                    "token and all.": null
                  },
                  "q": {
                    "type": "string",
                    "description": "The search the GIF was picked from (KLIPY's share signal)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored and ready.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/ChatMedia"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member (forbidden), may not post here (im_not_allowed; `information_barrier` in a direct message the company's information barriers keep apart), or file uploads off (file_uploads_disabled)."
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "gif_not_found / gif_invalid_slug / gif_too_large / gif_unavailable / attachment_cap_reached — error.message is the sentence to show."
          },
          "503": {
            "description": "gif_not_configured — this deployment has no KLIPY key."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/voice_notes": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Send a voice note",
        "description": "The recording becomes a message: the audio is its attachment (an\n`audio/*` file every client plays) and its words are transcribed —\nthe message's `transcript` is `{ status: \"pending\" }` until Whisper\nanswers, then `completed` with `text` (or `failed`, with `error` when\nthere is a sentence for the person). The change arrives as\n`message:updated` with `update_kind: \"transcript\"`. Same posting gate as\nPOST …/messages; `parent_message_id` in a channel makes it a thread\nreply. Up to 25 MB; webm, mp4/m4a, mp3, ogg or wav.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "audio"
                ],
                "properties": {
                  "audio": {
                    "type": "string",
                    "format": "binary"
                  },
                  "parent_message_id": {
                    "type": "integer"
                  },
                  "also_send_to_channel": {
                    "type": "boolean"
                  },
                  "push_to_talk": {
                    "type": "boolean",
                    "description": "Held to talk (walkie-talkie): the message's `push_to_talk` is true, and members whose `walkie_talkie` is on hear it play as it arrives. Always to the conversation — not with `parent_message_id`. A clip is at most 60 seconds: send its `duration_ms` (required with push_to_talk, at most 61000) in a compressed format of at most 3 MB — the server refuses a longer or larger clip with `push_to_talk_too_long`, and one with no `duration_ms` with `push_to_talk_duration_missing` (both 422). The push reads \"🎙 <name>: <transcript or 'Voice message'>\"."
                  },
                  "duration_ms": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "How long the recording is, in milliseconds, as the recorder measured it. Required with `push_to_talk` (at most 61000); optional for an ordinary voice note. Release 3 · kept on the audio attachment and answered as its `duration_ms` (a WebM file does not say how long it is); a value that is not a length (0, negative, over a day) keeps none and never refuses the voice note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The message it became.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member, may not post here (im_not_allowed; `information_barrier` in a direct message the company's information barriers keep apart), or file uploads off."
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "invalid_audio, file_too_large, attachment_cap_reached, parent_not_found, push_to_talk_reply (push_to_talk with parent_message_id), push_to_talk_duration_missing (push_to_talk without duration_ms) or push_to_talk_too_long (a push_to_talk clip over 61000 ms or 3 MB) — error.message is the sentence."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/draft": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's draft for this room",
        "description": "The same draft the web composer keeps as the person types, so a message started on one device is there on another. With `parent_message_id`, the reply draft of that channel thread — the one the web thread panel and the phone thread page keep.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "parent_message_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A channel thread's reply draft instead of the room's: the message replied to (a reply resolves to its thread's root)."
          }
        ],
        "responses": {
          "200": {
            "description": "The draft (`body` null when there is none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "$ref": "#/components/schemas/ChatDraft"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member"
          },
          "404": {
            "description": "Room not found, or parent_not_found (the thread's message is not in the room or was deleted)"
          },
          "422": {
            "description": "threads_not_available — `parent_message_id` on a room that is not a channel"
          }
        }
      },
      "put": {
        "tags": [
          "Chat"
        ],
        "summary": "Save the caller's draft for this room",
        "description": "`body` is what a send would post (mention markup included). A blank body clears the draft. Sending a message in the room clears it too. With `parent_message_id` (query or body) it is that channel thread's reply draft, cleared by a reply in the thread (POST …/messages with `parent_message_id` and `in_thread`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "parent_message_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A channel thread's reply draft instead of the room's: the message replied to (a reply resolves to its thread's root)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "mention_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "parent_message_id": {
                    "type": "integer",
                    "description": "Save the reply draft of this channel thread instead of the room's."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved (or cleared).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "$ref": "#/components/schemas/ChatDraft"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member"
          },
          "404": {
            "description": "Room not found, or parent_not_found (the thread's message is not in the room or was deleted)"
          },
          "422": {
            "description": "body_too_long, or threads_not_available — `parent_message_id` on a room that is not a channel"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Clear the caller's draft for this room",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "parent_message_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A channel thread's reply draft instead of the room's: the message replied to (a reply resolves to its thread's root)."
          }
        ],
        "responses": {
          "204": {
            "description": "Cleared"
          },
          "403": {
            "description": "Not a member"
          },
          "404": {
            "description": "Room not found, or parent_not_found (the thread's message is not in the room or was deleted)"
          },
          "422": {
            "description": "threads_not_available — `parent_message_id` on a room that is not a channel"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/polls": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Post a poll",
        "description": "A message whose body is the question, with the poll on it (`poll` on the message). Same posting gate as POST …/messages.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "question",
                  "options"
                ],
                "properties": {
                  "question": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "options": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "maxLength": 200
                    },
                    "description": "Distinct; blanks are dropped."
                  },
                  "allow_multiple": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The poll message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "`still_sending` — an `{ error }` body and no message: the poll is still being posted and arrives as `message:created`. Do not retry (it would post twice)."
          },
          "403": {
            "description": "Not a member, or may not post here (im_not_allowed; `information_barrier` in a direct message the company's information barriers keep apart)"
          },
          "422": {
            "description": "question_blank, question_too_long, too_few_options, too_many_options, option_too_long or duplicate_options."
          },
          "500": {
            "description": "`send_failed` — the poll was not posted; safe to try again."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/polls/{id}/vote": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Set the caller's answer to a poll",
        "description": "Sets the caller's votes to exactly `option_indexes` — one at most unless `allow_multiple`; `[]` takes the vote back. Members who may react here; an open poll only. Broadcasts `message:updated` with `update_kind \"poll\"`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The poll id (`poll.id`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "option_indexes": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 0
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The poll as the caller now sees it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "poll": {
                      "$ref": "#/components/schemas/ChatPoll"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "im_not_allowed; `information_barrier` in a direct message the company's information barriers keep apart"
          },
          "404": {
            "description": "poll_not_found"
          },
          "422": {
            "description": "poll_closed, invalid_option or one_answer_only."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/polls/{id}/close": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Close a poll",
        "description": "Its author or a room admin. A closed poll shows results only and can't be reopened, so closing asks the destructive:chat scope.",
        "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": "The closed poll.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "poll": {
                      "$ref": "#/components/schemas/ChatPoll"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "not_allowed; read_only (an archived channel or a frozen DM); `information_barrier` (a direct message the company's information barriers keep apart)"
          },
          "404": {
            "description": "poll_not_found"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/scheduled_messages": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's scheduled messages in this room",
        "description": "Still to send — scheduled, sending, and failed (with `error`) — soonest\nfirst, at most 100. Only their author sees them.\n\nRelease 3 · re-read it on `scheduled_messages:changed` (the caller's\ninbox channel, `{ room_id, scheduled_message_id, state }` — see\n`POST /chat/pusher/auth`): a change from any device, door or the minute\njob that sends what is due.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduled_messages": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "$ref": "#/components/schemas/ChatScheduledMessage"
                      }
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "The caller has more than the 100 returned here."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Schedule a message",
        "description": "Text only, from a minute to a year ahead. Posted at `send_at` as if sent then; the author's right to post is checked again then (a refusal leaves it `failed` with `error`, and a bell notice). `parent_message_id` in a channel makes it a thread reply. Clears the caller's draft for the room.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body",
                  "send_at"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "send_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "parent_message_id": {
                    "type": "integer"
                  },
                  "also_send_to_channel": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduled_message": {
                      "$ref": "#/components/schemas/ChatScheduledMessage"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member, or may not post here (im_not_allowed; `information_barrier` in a direct message the company's information barriers keep apart)"
          },
          "422": {
            "description": "invalid_body, invalid_send_at, parent_not_found, broadcast_not_allowed, or too_many_scheduled (the caller already has 100 open scheduled messages in this conversation)."
          }
        }
      }
    },
    "/chat/scheduled_messages/{id}": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Change a scheduled message's words or time",
        "description": "The author's, while scheduled or failed; a failed one is scheduled again.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string"
                  },
                  "send_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduled_message": {
                      "$ref": "#/components/schemas/ChatScheduledMessage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "not_found"
          },
          "409": {
            "description": "not_changeable — already sent or cancelled"
          },
          "422": {
            "description": "invalid_body or invalid_send_at"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Cancel a scheduled message",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled"
          },
          "404": {
            "description": "not_found"
          },
          "409": {
            "description": "not_changeable"
          }
        }
      }
    },
    "/chat/scheduled_messages/{id}/send_now": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Send a scheduled message now",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sent — `status` \"sent\" with `sent_message_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduled_message": {
                      "$ref": "#/components/schemas/ChatScheduledMessage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "not_found"
          },
          "409": {
            "description": "not_changeable"
          },
          "422": {
            "description": "Not sent now. Either the `{ scheduled_message }` body — `status` is \"failed\" and `error` says why — or an `{ error }` body with code `still_sending`: the post is still going through, the row stays \"sending\" and is posted (or marked failed) shortly; Send now meanwhile answers 409 `not_changeable`."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/mention_audience": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "How many people a message's @channel / @here / @group would notify",
        "description": "`@channel` notifies everyone in the conversation, `@here` everyone in it\nonline now, and `@[Group name](group:<id>)` the group's members who are\nin the conversation — never the sender, and nothing in a DM. Ask before\nsending and confirm past `confirm_above` (\"This will notify N people.\nSend anyway?\"). `allowed: false` means POST …/messages answers 422\n`broadcast_not_allowed` (Chat settings say who may use them; room\nadmins always may).\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"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "allowed": {
                      "type": "boolean"
                    },
                    "confirm_above": {
                      "type": "integer",
                      "example": 10
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/notifications": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Set the caller's notification level for a room",
        "description": "`all` notifies about every message, `mentions` only about what is\naddressed to the caller (an @mention, a direct message, a reply in a\nthread they follow, a message asking them to acknowledge it), `none`\nabout nothing — the room still counts unread messages but leaves the\napp-level unread total. `default` follows the room's default again: a\nchannel of more than 50 active members defaults to `mentions`, every\nother room to `all`. A timed mute (`/mute`) still silences everything\nwhile it lasts. The caller's own membership only.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "level"
                ],
                "properties": {
                  "level": {
                    "type": "string",
                    "enum": [
                      "all",
                      "mentions",
                      "none",
                      "default"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Level saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "notify_level": {
                      "type": "string",
                      "enum": [
                        "all",
                        "mentions",
                        "none"
                      ],
                      "description": "The level in force."
                    },
                    "notify_level_setting": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "all",
                        "mentions",
                        "none"
                      ],
                      "description": "What the caller chose; null follows the room's default."
                    },
                    "default_notify_level": {
                      "type": "string",
                      "enum": [
                        "all",
                        "mentions"
                      ],
                      "description": "The level with no setting (mentions for a channel of more than 50 members, else all) — the room payload's `default_notify_level`."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`invalid_level` — missing or not one of the four; `archived` — an archived channel is read-only, so it has no level to set."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/link_preview": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove a message's link preview card (sender only)",
        "description": "Stores `link_preview: {}` and tells every open room over\n`message:updated` (with `link_preview_only: true`; the body is\nunchanged). An edit that changes the message's first link fetches a\nnew card.\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": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer"
                    },
                    "link_preview": {
                      "type": "object",
                      "description": "Always empty."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a room member, or not the sender."
          },
          "404": {
            "description": "`message_not_found`"
          }
        }
      }
    },
    "/chat/status": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's status and Do Not Disturb",
        "description": "Whenever what this answer shows changes — from any door: the web\n\"Set a status\" drawer and Pause, the phone, `PUT`/`DELETE /chat/status`\nor Ask AI — the caller's own devices hear `status:changed`\n(`{ user_id }`) on `private-user-inbox-<user_id>-business-<business_id>`;\nre-read this endpoint. It goes to the caller's own channel only (other\npeople's statuses are re-read with `GET /chat/users/statuses`), and is\nnot sent for a save that changes nothing shown, when a status or pause\nruns out (time those from `expires_at` / `dnd_until`), or for\n`out_of_office`, which follows approved leave.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "$ref": "#/components/schemas/ChatOwnStatus"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "put": {
        "tags": [
          "Chat"
        ],
        "summary": "Set the caller's status and/or pause notifications",
        "description": "Only the keys sent change. `emoji` and `text` are the status, as a pair\n(one left out is blank; both blank clears it), clearing at\n`clear_after` (default `never`) or an exact `expires_at`. `dnd_for`\npauses notifications (`tomorrow` = 8:00 tomorrow in the caller's zone;\n`off` resumes), or an exact `dnd_until` (at most 7 days; blank\nresumes). While paused no chat push is sent; bell notifications still\narrive. Out of office is not set here: it follows approved leave. A\nchange tells the caller's own devices (`status:changed` — see `GET`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emoji": {
                    "type": "string",
                    "maxLength": 32,
                    "description": "One emoji."
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "clear_after": {
                    "type": "string",
                    "enum": [
                      "30_minutes",
                      "1_hour",
                      "4_hours",
                      "today",
                      "this_week",
                      "never"
                    ]
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "dnd_for": {
                    "type": "string",
                    "enum": [
                      "30_minutes",
                      "1_hour",
                      "2_hours",
                      "tomorrow",
                      "off"
                    ]
                  },
                  "dnd_until": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "$ref": "#/components/schemas/ChatOwnStatus"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "`invalid_status` — nothing sent, or a value refused (the message says which)."
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Clear the caller's status and resume notifications",
        "description": "Clears both. When something was showing, the caller's own devices hear\n`status:changed` (see `GET`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "$ref": "#/components/schemas/ChatOwnStatus"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/chat/users/statuses": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Other people's status",
        "description": "One entry per id that shows something (a status, paused notifications,\nor approved leave today); an id with nothing to show, outside this\ncompany, or of someone who shares no conversation with the caller, is\nabsent. A pause shows to others only as `dnd: true` —\nnever until when. At most 200 ids.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated user ids (or ids[]=…)."
          }
        ],
        "responses": {
          "200": {
            "description": "Statuses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "statuses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "integer"
                          },
                          "emoji": {
                            "type": "string",
                            "nullable": true
                          },
                          "text": {
                            "type": "string",
                            "nullable": true
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "dnd": {
                            "type": "boolean",
                            "description": "Notifications paused."
                          },
                          "out_of_office": {
                            "type": "boolean",
                            "description": "On approved leave today."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "422": {
            "description": "`too_many_ids`"
          }
        }
      }
    },
    "/chat/rooms/{id}/walkie_talkie": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Turn the caller's walkie-talkie on or off for a room",
        "description": "While it is on and the room is open, a push-to-talk clip (a voice note\nsent with `push_to_talk: true`; the message's `push_to_talk`) plays as\nit arrives — the web and phone pages do, and a native client that\nhonours the switch should. The caller's own membership only; any\nactive member may set it (listening needs no right to post), except in\nan archived channel and in the caller's own Note to self (nobody else\nis there to hear). The room's `walkie_talkie` and the caller's own\nmember row carry it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "on"
                ],
                "properties": {
                  "on": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Switch saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "walkie_talkie": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled or not a room member"
          },
          "404": {
            "description": "Room not found"
          },
          "422": {
            "description": "`missing_on` — no `on`; `archived` — an archived channel takes no clips; `note_to_self` — the caller's own Note to self has nobody else to hear one; `no_clips` — turning it ON where nobody can send a clip (the Needs you or Notifications conversation, a frozen one-to-one, one whose other person left or that an information barrier blocks). Turning it off is never refused."
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/pin": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Pin a message to its conversation",
        "description": "Anyone who may post in the room may pin, and its admins (an archived\nchannel is read-only). Not a room notice or a removed message. At most\n25 pins per room — the 26th is refused with `pin_limit`. Pinning posts\nthe room line \"X pinned a message.\"; every change is broadcast as\n`message:pinned` on `private-room-<id>` ({ id, room_id, pinned,\npinned_at, pinned_by: { id, name } | null, pinned_count }). Pinning a\npinned message changes nothing.\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": "The message, now pinned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    },
                    "pinned_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member, or may not pin here (`forbidden`; `information_barrier` in a direct message the company's information barriers keep apart)"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`not_pinnable` (a room notice) / `pin_limit` (25 already pinned)"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Unpin a message",
        "description": "Same rule as pinning. Posts no room line. A write, not a delete-level act.",
        "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": "The message, now unpinned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    },
                    "pinned_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member",
            "or may not pin here": null
          },
          "404": {
            "description": "`message_not_found` / room not found"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/save": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Save a message for later",
        "description": "A saved message is the caller's own bookmark (it also appears in their\nBookmarks). Not a room notice or a removed message. Idempotent.\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": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer"
                    },
                    "saved": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`not_savable` — a room notice"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove a message from saved",
        "description": "Always allowed — it is the caller's own row, whether or not they can still open the message. Idempotent.",
        "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": "Not saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": {
                      "type": "integer"
                    },
                    "saved": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/reminder": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Remind me about this message",
        "description": "Sets a reminder for the CALLER, delivered when due as a bell row and a\npush (\"Reminder: <author>'s message in <room>\", the message's words —\nmasked for a read-receipt request not yet acknowledged — and its\npermalink; the \"Chat · Reminders\" notification setting). No push while\nthe caller's notifications are paused (`PUT /chat/status` `dnd_for`);\nthe bell row still arrives. Presets are read in the caller's time zone.\nA message removed meanwhile, or a room the caller left, closes the\nreminder with no notice. The caller holds one reminder per message:\nasking again MOVES the open one to the new time (200, `moved: true`)\nrather than adding a second.\n\nOnce the bell row is written, the caller's own devices hear\n`reminder:due` on `private-user-inbox-<user_id>-business-<business_id>`:\n`{ reminder_id, room_id, message_id, room_name, excerpt }` — `excerpt`\nthe bell row's words (the read-receipt mask until acknowledged),\n`room_name` the conversation as the caller sees it — the room payload's\n`display_name` (an unnamed group by its people, never null). It is sent while notifications are paused too: whether to show\na notification for it is the client's, from the caller's own pause\n(`GET /chat/status` `dnd`). (`reminder`, not `remind`: …/remind is the\nsender's chase of unacknowledged recipients.)\n",
        "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",
                "properties": {
                  "preset": {
                    "type": "string",
                    "enum": [
                      "in_20_minutes",
                      "in_1_hour",
                      "in_3_hours",
                      "tomorrow_9am",
                      "next_monday_9am"
                    ]
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Instead of a preset — in the future",
                    "within a year.": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller's open reminder on this message was moved to the new time (`moved` true); same body as 201.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatMessageReminderSet"
                }
              }
            }
          },
          "201": {
            "description": "Reminder set (`moved` false)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatMessageReminderSet"
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`invalid_preset` / `invalid_time` / `not_remindable` (a chat notice) / `message_not_found`"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/forward": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Forward a message to other conversations",
        "description": "Posts a NEW message, as the caller, in each target: the note, then\n\"Forwarded from <author> in <room>:\", then the original's words as plain\ntext (mentions do not travel as mentions), then \"(N attachments in the\noriginal)\" — attachments stay in the original room. Up to 5 targets;\neach must be a conversation the caller can post in, and one that is not\nis `refused` with the reason while the rest still go.\n",
        "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": [
                  "room_ids"
                ],
                "properties": {
                  "room_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "minItems": 1,
                    "maxItems": 5
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What went where",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "forwarded": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "integer"
                          },
                          "message_id": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "refused": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "integer"
                          },
                          "reason": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "The sentence to show, in the caller's language — the web drawer's own: \"Forwarded to 2 conversations.\", then, for each refused conversation the caller is in, \"Not sent to #ops: <reason>\" (one they are not in is not named — its reason alone), or \"Nothing was forwarded.\" when every target was refused."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member of the message's room"
          },
          "404": {
            "description": "`message_not_found` / room not found"
          },
          "422": {
            "description": "`not_forwardable` (a room notice, a removed message, a read-receipt request the caller has not acknowledged) / `no_targets` / `too_many_targets` / `note_too_long`"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/mark_unread": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Mark a conversation unread from this message",
        "description": "Moves the caller's read position back to just before the message and\nrecounts their unread (live timeline messages by others); their other\nclients get `inbox:unread-changed`. Opening the room (or PATCH\n…/mark_read) reads it again — so a client should not mark the room read\nuntil the person sends, scrolls back to the newest message, or reopens it.\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": "Marked unread",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "message_id": {
                      "type": "integer"
                    },
                    "last_read_message_id": {
                      "type": "integer",
                      "description": "The previous timeline message",
                      "or 0.": null
                    },
                    "unread_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "`message_not_found` (also a removed message) / room not found"
          },
          "422": {
            "description": "`own_message` — the caller wrote it; `not_allowed` — it could not be marked unread (the caller stopped being an active member of the room meanwhile)"
          }
        }
      }
    },
    "/chat/rooms/{id}/pins": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The room's pinned messages",
        "description": "Newest pinned first (at most 25). Members only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pinned messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pins": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "pinned_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/rooms/{id}/on_shift": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Who in the room is on shift now",
        "description": "The room's members on shift right now — clocked in, else on a shift\nthey are expected at that covers now (the platform's one meaning of\n\"on shift\"), with the site they are on shift at. Members only, and only\nwhere the room's \"Who can see members?\" lets the caller. `@onshift` in a\nmessage notifies these people (a broadcast mention — the\n`can_broadcast_mentions` capability). `people` lists at most 200, by\nname; `total_count` is everyone, and `truncated` says the list was cut.\n\nRelease 3 · each person says what they are working now — `role`,\n`shift_starts_at`, `shift_ends_at`, `shift_time_zone` (their current\nshift; see `ChatOnShiftPerson`). The web panel draws them as\n\"Shift Lead · 6:00 AM – 2:30 PM\" on the shift's clock, labelled when it\nis not the viewer's (\"… 4:00 PM PDT\"). The three are null for a person\nwhose shift the caller may not see (their own, their reports', their\nlocations'; admins see all).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The members on shift now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "on_shift_count": {
                      "type": "integer"
                    },
                    "people": {
                      "type": "array",
                      "maxItems": 200,
                      "items": {
                        "$ref": "#/components/schemas/ChatOnShiftPerson"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Everyone on shift",
                      "before the 200 cap.": null
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "More than 200 — `people` was cut."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a room member (`forbidden`), or the roster is kept to admins (`members_hidden`)"
          },
          "404": {
            "description": "Room not found"
          }
        }
      }
    },
    "/chat/on_shift": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Who holds a role on shift now at a location",
        "description": "\"Message who's on shift\": the people working `role` (a role type, as\non a shift — `GET /chat/on_shift/options` lists them) at the location\nor a site beneath it right now. The location is one of the caller's\nown (the locations they are assigned to and the sites beneath them) —\nany active location for a manager or above, or a Chat admin. Start a chat with them through\n`POST /chat/start_chat` (one person → a DM, several → a group of up to\n20; `group_too_large` says when there are more). When nobody holds the\nrole now, `people` is empty and `room` is the location's channel, when\nthere is one the caller can open. `people` lists at most 200, by name;\n`total_count` is everyone, and `truncated` says the list was cut.\n\nRelease 3 · each person says what they are working now — `role`,\n`shift_starts_at`, `shift_ends_at`, `shift_time_zone`: their current\nshift at this location or a site beneath it (see `ChatOnShiftPerson`\n— null where the caller may not see it), as the web drawer shows it.\n`location` stays the location asked about.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A role type; blank = anyone on shift there."
          }
        ],
        "responses": {
          "200": {
            "description": "Who that is right now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "location": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "role": {
                      "type": "string",
                      "nullable": true
                    },
                    "people": {
                      "type": "array",
                      "maxItems": 200,
                      "items": {
                        "$ref": "#/components/schemas/ChatOnShiftPerson"
                      }
                    },
                    "total_count": {
                      "type": "integer",
                      "description": "Everyone who holds the role there now (after the information-barrier cut)",
                      "before the 200 cap.": null
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "More than 200 — `people` was cut."
                    },
                    "group_too_large": {
                      "type": "boolean",
                      "description": "More people than a group holds (20 with the caller)."
                    },
                    "barred_count": {
                      "type": "integer",
                      "description": "How many people on shift there now are left out of `people` because the company's information barriers keep the caller from them — say so (\"Nobody you can message is on shift…\" when `people` is empty, else \"N people on shift can't be messaged under your company's policy.\"), never that nobody is on shift."
                    },
                    "room": {
                      "nullable": true,
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatRoom"
                        }
                      ],
                      "description": "The location's channel, offered when nobody is on shift in that role (or there are too many for a group)."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`start_chat_disabled` — direct messages and group chats are both off; `on_shift_unavailable` — none of Shifts & Scheduling, Shift Marketplace or Time & Attendance is on, so nobody can be on shift"
          },
          "404": {
            "description": "`location_not_found` — no such active location, or not one of the caller's (a manager or above, or a Chat admin, may ask about any)"
          },
          "422": {
            "description": "`location_required`"
          }
        }
      }
    },
    "/chat/on_shift/options": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Locations and roles to pick for \"Message who's on shift\"",
        "description": "The caller's own locations by name (any active location for a manager\nor above, or a Chat admin), at most 1,000; `locations_truncated` says\nthere are more, and `q` finds them by name.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only the locations whose name holds every word of this, in any order (accents ignored)."
          }
        ],
        "responses": {
          "200": {
            "description": "What to pick from",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "locations_truncated": {
                      "type": "boolean",
                      "description": "There are more locations than listed — search them with `q`."
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The role types people here hold (the list on-shift groups offer)",
                      "as stored — the values `GET /chat/on_shift?role=` takes.": null
                    },
                    "role_options": {
                      "type": "array",
                      "description": "Release 3 · the same roles to draw a picker from: `value` as stored (send it as `?role=`), `label` the name people read (a stored key `crew_lead` reads \"Crew Lead\") — what a person's `role` says and the web picker shows.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {
                            "type": "string",
                            "example": "crew_lead"
                          },
                          "label": {
                            "type": "string",
                            "example": "Crew Lead"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`start_chat_disabled` — direct messages and group chats are both off; `on_shift_unavailable` — none of Shifts & Scheduling, Shift Marketplace or Time & Attendance is on, so nobody can be on shift"
          }
        }
      }
    },
    "/chat/rooms/{id}/org_sync": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Stop a channel syncing its members",
        "description": "A channel that follows a group, a location, a team or a shift stops\nfollowing it. Everyone in it stays; from then on people are added and\nremoved by hand (`org_source` and `synced_from` become null), and one\nline in the channel says who stopped it. Chat never makes another\nchannel for that location or manager. The channel's admins and the\ncompany's Chat admins (`can_stop_org_sync`). Nothing is deleted, so it\nasks `write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The channel, no longer synced",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "$ref": "#/components/schemas/ChatRoom"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not the channel's admin or a Chat admin"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_a_channel`, `archived` (restore it first) or `not_synced` (its members already aren't synced)"
          }
        }
      }
    },
    "/chat/rooms/{id}/legal_hold": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Place a legal hold on a conversation",
        "description": "Keeps everything in the conversation for the company's records until\nthe hold is lifted: nothing in it is permanently deleted, and it can't\nbe deleted for everyone (422 `legal_hold`). Messages past the company's\nretention window are still hidden from people, as everywhere. People\ncan still delete their own messages; the words are kept. Only\nbusiness (company) admins see that a room is on hold (`on_legal_hold`).\nA business admin under write:chat, whether or not they are in the\nconversation: a conversation is held by its id without joining it, and\nnothing in it tells its members (`can_manage_legal_hold` on the room\npayload says the room's own menu offers it to an admin who is in it).\nThe hold is listed in Admin › Retention & Legal Holds with every other\nhold, which can place it the same way.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "The matter the hold is for."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Placed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatRoom"
                        }
                      ],
                      "nullable": true,
                      "description": "The room for a caller who is in it; null for a business admin who is not (what is said in it is never returned to someone outside it)."
                    },
                    "legal_hold": {
                      "$ref": "#/components/schemas/ChatLegalHold"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not a business admin, or a token that may not act as one"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`reason_required`, `already_held` (one hold on a conversation at a time), or `invalid`"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Lift the legal hold on a conversation",
        "description": "Lifts the hold placed on this conversation; the company's retention\nsettings delete its hidden messages for good again. A hold on someone in it (or on one of its\nmessages) is lifted in Admin › Retention & Legal Holds, and keeps the\nroom held (`on_legal_hold` stays true). Nothing is deleted, so it asks\nwrite:chat. A reason is required, as on every door that lifts a hold\n(the web drawer, the phone, Admin › Retention & Legal Holds).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "lift_reason",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 1000
            },
            "description": "Why it is lifted (kept on the hold). Required."
          }
        ],
        "responses": {
          "200": {
            "description": "Lifted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatRoom"
                        }
                      ],
                      "nullable": true,
                      "description": "The room for a caller who is in it; null for a business admin who is not (what is said in it is never returned to someone outside it)."
                    },
                    "legal_hold": {
                      "$ref": "#/components/schemas/ChatLegalHold"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not a business admin, or a token that may not act as one"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_held` — no hold is placed on this conversation; `reason_required` — no `lift_reason` was given (\"Give a reason for lifting the hold.\")"
          }
        }
      }
    },
    "/chat/saved": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's saved messages",
        "description": "Newest saved first, paged by cursor (`before` = the previous page's\n`next_before`). A saved message the caller can no longer open (a room\nthey left, a message removed) comes back `available: false` with no\n`message` or `room` — the save is still theirs to remove.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Saved messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The save's id."
                          },
                          "saved_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "available": {
                            "type": "boolean"
                          },
                          "message_id": {
                            "type": "integer"
                          },
                          "room": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "room_type": {
                                "type": "string",
                                "enum": [
                                  "direct",
                                  "group",
                                  "channel"
                                ]
                              }
                            }
                          },
                          "message": {
                            "nullable": true,
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/ChatMessage"
                              }
                            ]
                          }
                        }
                      }
                    },
                    "next_before": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/chat/saved/{message_id}": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove a saved message by its id",
        "description": "The Saved view's Remove: takes the caller's own save of the message off\nwithout naming its room, so it works for a row that came back\n`available: false` (a room the caller left, or that was deleted) as for\nany other. The same removal as `DELETE /chat/rooms/{room_id}/messages/{message_id}/save`.\nIdempotent — a message the caller has not saved answers 204 too.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Not saved (any more)"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled",
            "or a token without `write:chat`": null
          }
        }
      }
    },
    "/chat/reminders": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The caller's open message reminders",
        "description": "Soonest first (at most 100).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reminders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reminders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessageReminder"
                      }
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "The caller has more than the 100 returned."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/chat/reminders/{id}": {
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Cancel a message reminder",
        "description": "The caller's own reminder (or one they set). A write, not a delete-level act.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "cancelled": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`reminder_not_found`"
          }
        }
      }
    },
    "/chat/cards/{id}/decide": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Press the button on a card in Needs you",
        "description": "A message from the Needs you service account (named Approvals until\n2026-10-22) carries `card`: something waiting on the caller.\n\nAn approval (`kind: approval`, verdict `approve` / `reject`) is decided\nthrough the approvals registry as the caller — the same gates as the\napprovals page and POST /api/v1/home/waiting_on_you/decide: the queue's\ndoor, the record in the caller's queue right now, then the owning app's\nown action. Needs `write:chat` and `write:approvals`, plus the area's\ndelete scope for a delete-level verdict (deny leave, reject a contract\n…). A decline the queue asks a reason for is refused\n(`reason_required`) — open the item (`card.row.deep_link`) to decline\nit with one.\n\nAn Inbox ask's card (`kind: acknowledge` / `sign`; the verdict is the\nkind) is answered through the Inbox's own handler, as POST\n/api/v1/notifications/{id}/actions/{action_id}/perform would: a\nsign-off needs `write:approvals`, an acknowledgement\n`write:notifications`. One that asks the caller to affirm statements\nfirst is refused (`open_to_finish`, `card.act_needs_app`) — open the\nitem to finish it there. A notice card (`kind: notice` — a safety\nincident at a location the caller manages) takes `seen`\n(`write:notifications`): the notice is read. A link card (`kind:\nlink`) has no button: open `row.deep_link`.\n\nA card in a channel (`kind: open_shift`, from the OneMango account) is\nanyone's in the channel: `claim` claims the shift as the caller, with\nthe claim's own rules (`write:shifts`) — `card.pending_approval` while\na manager still decides.\n\nEvery open room hears the change over `message:updated` with\n`update_kind \"card\"`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The card's id (`message.card.id`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verdict"
                ],
                "properties": {
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "reject",
                      "acknowledge",
                      "sign",
                      "seen",
                      "claim"
                    ],
                    "description": "`approve` / `reject` on an approval card; the card's `kind` on an Inbox ask's card; `seen` on a notice card; `claim` on an open shift."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided, acknowledged or signed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "The owning app's own sentence."
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "card": {
                      "$ref": "#/components/schemas/ChatMessageCard"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (a token without the scope the card asks — write:approvals, write:notifications for an acknowledgement or a notice, write:shifts for an open shift — or the delete scope) or `queue_unavailable`"
          },
          "404": {
            "description": "`card_not_found` — not the caller's card, or its message is gone"
          },
          "409": {
            "description": "`already_decided`, or `not_waiting` (decided elsewhere, or no longer waiting on the caller — the returned `card` now reads \"Already handled\")"
          },
          "422": {
            "description": "`reason_required`, `open_to_finish` (an acknowledgement that asks for statements first), `invalid_verdict` or `decision_refused` (the app said no; `error.message` says why). The body carries `card`."
          }
        }
      }
    },
    "/chat/rooms/{id}/app_events": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "A channel's app updates",
        "description": "Which app updates post into this channel as cards, from the OneMango\naccount: open shifts and coverage gaps at its location, team members\nrecognised, and Celebrations — one card a day with the people the\ncompany celebrated that day (work anniversaries, welcomes, promotions,\ncertificates, contest winners). An org channel starts with its\nsource's (a store channel: open shifts and coverage gaps; a team\nchannel: recognition); any other with none, and no channel takes\nCelebrations until its admins turn it on. Anyone in the channel may\nread it; `can_change` says whether the caller may change it (the\nchannel's admins and the company's Chat admins).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatRoomAppEvents"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not in the channel"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_a_channel`"
          }
        }
      },
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Change a channel's app updates",
        "description": "The whole list to keep — an empty one turns every update off. The channel's admins and the company's Chat admins only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "events"
                ],
                "properties": {
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "open_shifts",
                        "coverage_gaps",
                        "team_recognition",
                        "celebrations"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The list as kept",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatRoomAppEvents"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not one of the channel's admins"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_a_channel`, `archived` (an archived channel's app updates cannot change) or `missing_events`"
          }
        }
      }
    },
    "/chat/rooms/{id}/webhooks": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "A channel's incoming webhooks",
        "description": "The channel's live incoming webhooks, newest first: a URL another tool\nposts into the channel with — Slack's incoming-webhook JSON, or an AWS\nSNS topic's HTTPS subscription. Each post comes from the company's\nOneMango account, headed with the webhook's name. The channel's admins\nand the company's Chat admins only (an archived channel's too, to\nrevoke them). A webhook's URL is never listed: only its last four\ncharacters (`url_hint`). `enabled` is the company's \"Incoming\nwebhooks\" switch (Chat settings); while it is off every URL answers 403\nand none can be added.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "enabled": {
                      "type": "boolean"
                    },
                    "can_add": {
                      "type": "boolean",
                      "description": "false on an archived channel."
                    },
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatIncomingWebhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not one of the channel's admins"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_a_channel`"
          }
        }
      },
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Add an incoming webhook to a channel",
        "description": "Answers the webhook and its `url` — the only time the URL is given: only\na digest of its token is kept. Anyone with the URL can POST into the\nchannel: Slack's message JSON (`{\"text\": …}`, attachments, blocks), or\nan AWS SNS subscription (it confirms itself). The channel is told it was\nadded. Up to 20 per channel.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Heads every post the webhook makes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/ChatIncomingWebhook"
                    },
                    "url": {
                      "type": "string",
                      "description": "https://hooks.<base>/chat/webhooks/<token> — shown this once."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not one of the channel's admins; `incoming_webhooks_disabled` — the company's switch is off"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`not_a_channel`, `archived`, `invalid` (no name, or longer than 80) or `limit` (20 already)"
          }
        }
      }
    },
    "/chat/rooms/{id}/webhooks/{webhook_id}": {
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Rename an incoming webhook",
        "description": "The new name heads its posts from now on; its URL does not change. The channel is told. Its admins and Chat admins may rename one, as they may revoke it — on an archived channel too, and while the company's incoming webhooks are switched off; a name of up to 80 characters. The same name again changes nothing (`renamed` false).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Heads every post the webhook makes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renamed (or already that name)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/ChatIncomingWebhook"
                    },
                    "previous_name": {
                      "type": "string",
                      "description": "The name it had before this request."
                    },
                    "renamed": {
                      "type": "boolean",
                      "description": "false when it already had that name — nothing changed and the channel was not told."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not one of the channel's admins"
          },
          "404": {
            "description": "`room_not_found` or `webhook_not_found` (none live with that id here)"
          },
          "422": {
            "description": "`not_a_channel` or `invalid` (no name, or longer than 80)"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Revoke an incoming webhook",
        "description": "Its URL answers 404 from now on, for good; the channel is told. Asks `destructive:chat` (a final act).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/ChatIncomingWebhook"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — not one of the channel's admins"
          },
          "404": {
            "description": "`room_not_found` or `webhook_not_found` (none live with that id here)"
          },
          "422": {
            "description": "`not_a_channel`"
          }
        }
      }
    },
    "/chat/notification_mirror": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "\"Also send my notifications to Chat\" — the caller's setting",
        "description": "Off until the person turns it on. While on, each new notification of a\nchosen kind is copied into their DM with the Notifications service\naccount: its title, an excerpt and its link — never more than the\nnotification carried. Copies are silent (no push, no notice): the DM's\nunread badge is all they add. Chat's own notifications are never\ncopied. `available` is the notification settings page's own list of\nkinds, less Chat's.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The setting",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatNotificationMirror"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Chat"
        ],
        "summary": "Turn \"Also send my notifications to Chat\" on or off",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Omitted keeps the switch where it is."
                  },
                  "categories": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Keys from `available`. Omitted keeps the kinds chosen before."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The setting after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatNotificationMirror"
                }
              }
            }
          },
          "409": {
            "description": "`retired` — the company's \"One interruption per item\" is on, so notifications are not copied to Chat"
          },
          "422": {
            "description": "`unknown_category` (a key not in `available`) or `no_categories` (on, with none chosen)"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/escalation": {
      "put": {
        "tags": [
          "Chat"
        ],
        "summary": "Set or change the backup on your own Important message",
        "description": "The sender only, on their Important message in a DM or group, until the\nbackup has been told. Same rules as `escalate_to_user_id` /\n`escalate_after_minutes` on send. A backup set or changed here waits\nfrom now, not from when the message was sent; a wait that would end\nafter the message is 14 days old is refused (`too_late`).\n",
        "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": [
                  "escalate_to_user_id",
                  "escalate_after_minutes"
                ],
                "properties": {
                  "escalate_to_user_id": {
                    "type": "integer"
                  },
                  "escalate_after_minutes": {
                    "type": "integer",
                    "enum": [
                      60,
                      120,
                      240,
                      1440
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "escalation": {
                      "$ref": "#/components/schemas/ChatMessageEscalation"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`message_not_found` — not the caller's message"
          },
          "409": {
            "description": "`already_escalated`"
          },
          "422": {
            "description": "A refused backup or wait (the codes above), `escalation_needs_important`, or `too_late`"
          }
        }
      },
      "delete": {
        "tags": [
          "Chat"
        ],
        "summary": "Take the backup off your own Important message",
        "description": "Until the backup has been told. A write, not a delete-level act.",
        "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": "Taken off (`escalation` is null)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "escalation": {
                      "nullable": true,
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ChatMessageEscalation"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`message_not_found`"
          },
          "409": {
            "description": "`already_escalated`"
          },
          "422": {
            "description": "`not_set`"
          }
        }
      }
    },
    "/chat/rooms/{id}/permissions": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "A room's permission settings and what the caller may do",
        "description": "The room's five stored codes, the caller's resolved capabilities, and\nwhether the caller may change the settings — without GET\n/chat/rooms/{id}'s message window or its mark-read side effect. Any\nactive member may read; a member who is not a room admin gets\n`editable: false` (show the dropdowns disabled, not hidden). The same\ncodes ride on every serialized room.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settings, capabilities and what the caller may change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "room_type": {
                      "type": "string",
                      "enum": [
                        "direct",
                        "group",
                        "channel"
                      ]
                    },
                    "settings": {
                      "type": "object",
                      "description": "The stored code for each setting, or its default (`defaults`) where none is stored. \"Admins\" means room admins and business admins.",
                      "properties": {
                        "other_member_invite": {
                          "type": "string",
                          "enum": [
                            "Y",
                            "N"
                          ],
                          "description": "Who can add new members: Y any member, N admins only."
                        },
                        "can_view_member": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can see the member list: B any member, G admins only."
                        },
                        "im_permission": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send messages: B any member, G admins only, S business admins only."
                        },
                        "important_msg_sender": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send important messages and request read receipts: B any member, G admins only, S business admins only."
                        },
                        "thread_replies": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can reply in threads: B any member, G admins only. Read only in a channel."
                        }
                      }
                    },
                    "capabilities": {
                      "type": "object",
                      "description": "What the caller may do here, judged under the token. A\ntoken without write:chat is offered no write (every key\nbut `can_see_members` is false); without destructive:chat,\n`can_delete_for_everyone`, `can_archive`, `can_leave` and\n`can_close_polls` are false.\n`can_send_important_msg` and `can_request_read_receipt`\nalso need the company's Important messages / Read receipt\nrequests setting on and a DM, a group, or a private\nchannel of up to 50 people. `can_add_members` is false in\na DM.\n",
                      "properties": {
                        "can_add_members": {
                          "type": "boolean"
                        },
                        "can_see_members": {
                          "type": "boolean"
                        },
                        "can_send_im": {
                          "type": "boolean"
                        },
                        "can_send_important_msg": {
                          "type": "boolean"
                        },
                        "can_request_read_receipt": {
                          "type": "boolean"
                        },
                        "can_reply_in_thread": {
                          "type": "boolean",
                          "description": "Channels only."
                        },
                        "can_archive": {
                          "type": "boolean"
                        },
                        "can_make_private": {
                          "type": "boolean"
                        },
                        "can_make_public": {
                          "type": "boolean"
                        },
                        "can_edit_details": {
                          "type": "boolean"
                        },
                        "can_delete_for_everyone": {
                          "type": "boolean"
                        },
                        "can_pin": {
                          "type": "boolean"
                        },
                        "can_broadcast_mentions": {
                          "type": "boolean"
                        },
                        "can_close_polls": {
                          "type": "boolean"
                        },
                        "can_leave": {
                          "type": "boolean"
                        },
                        "can_stop_org_sync": {
                          "type": "boolean"
                        },
                        "can_manage_webhooks": {
                          "type": "boolean"
                        }
                      }
                    },
                    "is_room_admin": {
                      "type": "boolean"
                    },
                    "is_team_admin": {
                      "type": "boolean",
                      "description": "Same value as `is_room_admin` (the messenger's older name)."
                    },
                    "editable": {
                      "type": "boolean",
                      "description": "The caller may save changes — a room admin, not in a DM, on a token with write:chat. Gate the Save button on this."
                    },
                    "locked": {
                      "type": "object",
                      "description": "Per setting, true when the caller may NOT change it: every\nkey while `editable` is false, and — for a room admin who\nis not a business admin — any key holding `S`. Show that\ndropdown disabled; the PATCH would answer 403.\n",
                      "additionalProperties": {
                        "type": "boolean"
                      }
                    },
                    "allowed_values": {
                      "type": "object",
                      "description": "Every code each setting accepts, to build the dropdowns from.",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "example": {
                        "other_member_invite": [
                          "Y",
                          "N"
                        ],
                        "can_view_member": [
                          "B",
                          "G"
                        ],
                        "im_permission": [
                          "B",
                          "S",
                          "G"
                        ],
                        "important_msg_sender": [
                          "B",
                          "S",
                          "G"
                        ],
                        "thread_replies": [
                          "B",
                          "G"
                        ]
                      }
                    },
                    "defaults": {
                      "type": "object",
                      "description": "The code a room holds when nothing is stored.",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "other_member_invite": "Y",
                        "can_view_member": "B",
                        "im_permission": "B",
                        "important_msg_sender": "B",
                        "thread_replies": "B"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a room member; or Chat app disabled"
          },
          "404": {
            "description": "`room_not_found`"
          }
        }
      },
      "patch": {
        "tags": [
          "Chat"
        ],
        "summary": "Change a room's permission settings (room admins)",
        "description": "Room admins only. Send any of the five settings at the top level of\nthe body; settings left out are unchanged, so one dropdown can be saved\nalone. Codes are exactly as listed (upper-case); one the setting does\nnot take is refused and nothing is written. Setting a key to `S`, or\nchanging one that holds `S`, needs a business admin — a room admin who\nis not one gets 403 `permission_tier_not_allowed` (resending the `S`\nalready stored is allowed). Each setting that actually changes posts a\nroom line (\"Dana Lee changed who can send messages to: Domain admins\nonly.\"). Not in a DM.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRoomPermissionCodes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved — the same shape as GET, after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "room_type": {
                      "type": "string",
                      "enum": [
                        "direct",
                        "group",
                        "channel"
                      ]
                    },
                    "settings": {
                      "type": "object",
                      "description": "The stored code for each setting, or its default (`defaults`) where none is stored. \"Admins\" means room admins and business admins.",
                      "properties": {
                        "other_member_invite": {
                          "type": "string",
                          "enum": [
                            "Y",
                            "N"
                          ],
                          "description": "Who can add new members: Y any member, N admins only."
                        },
                        "can_view_member": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can see the member list: B any member, G admins only."
                        },
                        "im_permission": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send messages: B any member, G admins only, S business admins only."
                        },
                        "important_msg_sender": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send important messages and request read receipts: B any member, G admins only, S business admins only."
                        },
                        "thread_replies": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can reply in threads: B any member, G admins only. Read only in a channel."
                        }
                      }
                    },
                    "capabilities": {
                      "type": "object",
                      "description": "What the caller may do here, judged under the token. A\ntoken without write:chat is offered no write (every key\nbut `can_see_members` is false); without destructive:chat,\n`can_delete_for_everyone`, `can_archive`, `can_leave` and\n`can_close_polls` are false.\n`can_send_important_msg` and `can_request_read_receipt`\nalso need the company's Important messages / Read receipt\nrequests setting on and a DM, a group, or a private\nchannel of up to 50 people. `can_add_members` is false in\na DM.\n",
                      "properties": {
                        "can_add_members": {
                          "type": "boolean"
                        },
                        "can_see_members": {
                          "type": "boolean"
                        },
                        "can_send_im": {
                          "type": "boolean"
                        },
                        "can_send_important_msg": {
                          "type": "boolean"
                        },
                        "can_request_read_receipt": {
                          "type": "boolean"
                        },
                        "can_reply_in_thread": {
                          "type": "boolean",
                          "description": "Channels only."
                        },
                        "can_archive": {
                          "type": "boolean"
                        },
                        "can_make_private": {
                          "type": "boolean"
                        },
                        "can_make_public": {
                          "type": "boolean"
                        },
                        "can_edit_details": {
                          "type": "boolean"
                        },
                        "can_delete_for_everyone": {
                          "type": "boolean"
                        },
                        "can_pin": {
                          "type": "boolean"
                        },
                        "can_broadcast_mentions": {
                          "type": "boolean"
                        },
                        "can_close_polls": {
                          "type": "boolean"
                        },
                        "can_leave": {
                          "type": "boolean"
                        },
                        "can_stop_org_sync": {
                          "type": "boolean"
                        },
                        "can_manage_webhooks": {
                          "type": "boolean"
                        }
                      }
                    },
                    "is_room_admin": {
                      "type": "boolean"
                    },
                    "is_team_admin": {
                      "type": "boolean",
                      "description": "Same value as `is_room_admin` (the messenger's older name)."
                    },
                    "editable": {
                      "type": "boolean",
                      "description": "The caller may save changes — a room admin, not in a DM, on a token with write:chat. Gate the Save button on this."
                    },
                    "locked": {
                      "type": "object",
                      "description": "Per setting, true when the caller may NOT change it: every\nkey while `editable` is false, and — for a room admin who\nis not a business admin — any key holding `S`. Show that\ndropdown disabled; the PATCH would answer 403.\n",
                      "additionalProperties": {
                        "type": "boolean"
                      }
                    },
                    "allowed_values": {
                      "type": "object",
                      "description": "Every code each setting accepts, to build the dropdowns from.",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "example": {
                        "other_member_invite": [
                          "Y",
                          "N"
                        ],
                        "can_view_member": [
                          "B",
                          "G"
                        ],
                        "im_permission": [
                          "B",
                          "S",
                          "G"
                        ],
                        "important_msg_sender": [
                          "B",
                          "S",
                          "G"
                        ],
                        "thread_replies": [
                          "B",
                          "G"
                        ]
                      }
                    },
                    "defaults": {
                      "type": "object",
                      "description": "The code a room holds when nothing is stored.",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "other_member_invite": "Y",
                        "can_view_member": "B",
                        "im_permission": "B",
                        "important_msg_sender": "B",
                        "thread_replies": "B"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a room member, or not a room admin; `permission_tier_not_allowed` — only a business admin may set or clear `S`; or Chat app disabled / no write:chat"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`direct_room` (a DM has no settings — answered only to a member: a non-member gets the 403 `forbidden` first), `nothing_to_update` (none of the five settings sent), or `invalid_permission_setting` (a code that setting does not take)"
          }
        }
      },
      "put": {
        "tags": [
          "Chat"
        ],
        "summary": "Change a room's permission settings (PUT)",
        "description": "Same as PATCH on this path — the same body, rules and responses.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRoomPermissionCodes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved — the same shape as GET, after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "room_type": {
                      "type": "string",
                      "enum": [
                        "direct",
                        "group",
                        "channel"
                      ]
                    },
                    "settings": {
                      "type": "object",
                      "description": "The stored code for each setting, or its default (`defaults`) where none is stored. \"Admins\" means room admins and business admins.",
                      "properties": {
                        "other_member_invite": {
                          "type": "string",
                          "enum": [
                            "Y",
                            "N"
                          ],
                          "description": "Who can add new members: Y any member, N admins only."
                        },
                        "can_view_member": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can see the member list: B any member, G admins only."
                        },
                        "im_permission": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send messages: B any member, G admins only, S business admins only."
                        },
                        "important_msg_sender": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G",
                            "S"
                          ],
                          "description": "Who can send important messages and request read receipts: B any member, G admins only, S business admins only."
                        },
                        "thread_replies": {
                          "type": "string",
                          "enum": [
                            "B",
                            "G"
                          ],
                          "description": "Who can reply in threads: B any member, G admins only. Read only in a channel."
                        }
                      }
                    },
                    "capabilities": {
                      "type": "object",
                      "description": "What the caller may do here, judged under the token. A\ntoken without write:chat is offered no write (every key\nbut `can_see_members` is false); without destructive:chat,\n`can_delete_for_everyone`, `can_archive`, `can_leave` and\n`can_close_polls` are false.\n`can_send_important_msg` and `can_request_read_receipt`\nalso need the company's Important messages / Read receipt\nrequests setting on and a DM, a group, or a private\nchannel of up to 50 people. `can_add_members` is false in\na DM.\n",
                      "properties": {
                        "can_add_members": {
                          "type": "boolean"
                        },
                        "can_see_members": {
                          "type": "boolean"
                        },
                        "can_send_im": {
                          "type": "boolean"
                        },
                        "can_send_important_msg": {
                          "type": "boolean"
                        },
                        "can_request_read_receipt": {
                          "type": "boolean"
                        },
                        "can_reply_in_thread": {
                          "type": "boolean",
                          "description": "Channels only."
                        },
                        "can_archive": {
                          "type": "boolean"
                        },
                        "can_make_private": {
                          "type": "boolean"
                        },
                        "can_make_public": {
                          "type": "boolean"
                        },
                        "can_edit_details": {
                          "type": "boolean"
                        },
                        "can_delete_for_everyone": {
                          "type": "boolean"
                        },
                        "can_pin": {
                          "type": "boolean"
                        },
                        "can_broadcast_mentions": {
                          "type": "boolean"
                        },
                        "can_close_polls": {
                          "type": "boolean"
                        },
                        "can_leave": {
                          "type": "boolean"
                        },
                        "can_stop_org_sync": {
                          "type": "boolean"
                        },
                        "can_manage_webhooks": {
                          "type": "boolean"
                        }
                      }
                    },
                    "is_room_admin": {
                      "type": "boolean"
                    },
                    "is_team_admin": {
                      "type": "boolean",
                      "description": "Same value as `is_room_admin` (the messenger's older name)."
                    },
                    "editable": {
                      "type": "boolean",
                      "description": "The caller may save changes — a room admin, not in a DM, on a token with write:chat. Gate the Save button on this."
                    },
                    "locked": {
                      "type": "object",
                      "description": "Per setting, true when the caller may NOT change it: every\nkey while `editable` is false, and — for a room admin who\nis not a business admin — any key holding `S`. Show that\ndropdown disabled; the PATCH would answer 403.\n",
                      "additionalProperties": {
                        "type": "boolean"
                      }
                    },
                    "allowed_values": {
                      "type": "object",
                      "description": "Every code each setting accepts, to build the dropdowns from.",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "example": {
                        "other_member_invite": [
                          "Y",
                          "N"
                        ],
                        "can_view_member": [
                          "B",
                          "G"
                        ],
                        "im_permission": [
                          "B",
                          "S",
                          "G"
                        ],
                        "important_msg_sender": [
                          "B",
                          "S",
                          "G"
                        ],
                        "thread_replies": [
                          "B",
                          "G"
                        ]
                      }
                    },
                    "defaults": {
                      "type": "object",
                      "description": "The code a room holds when nothing is stored.",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "other_member_invite": "Y",
                        "can_view_member": "B",
                        "im_permission": "B",
                        "important_msg_sender": "B",
                        "thread_replies": "B"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a room member, or not a room admin; `permission_tier_not_allowed` — only a business admin may set or clear `S`; or Chat app disabled / no write:chat"
          },
          "404": {
            "description": "`room_not_found`"
          },
          "422": {
            "description": "`direct_room` (a DM has no settings — answered only to a member: a non-member gets the 403 `forbidden` first), `nothing_to_update` (none of the five settings sent), or `invalid_permission_setting` (a code that setting does not take)"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/ask_ai/summarize": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Ask the assistant to summarize a thread or the room's recent conversation",
        "description": "With `thread_root_id`, summarizes that thread; without it, the room's\nrecent conversation — the last 24 hours, at most 100 messages, unless\n`window` says otherwise (`window` is ignored with `thread_root_id`).\nAnswers 202 at once; the summary is posted to the room later as a\nnormal Ask AI message whose `ai_affordances.provenance` is `summary` (a\nchannel thread's summary is a reply in that thread). Because it is a\npost every member reads, the caller must be allowed to post here — in\na channel thread, to reply in it. At most 5 summaries per room per\nhour, thread and room summaries together. Needs the Chat settings\n`ask_ai_enabled` and `ask_ai_summaries_enabled`, and Platform Standard\nor above.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "thread_root_id": {
                    "type": "integer",
                    "description": "A live top-level message in this room. Omit to summarize the room."
                  },
                  "window": {
                    "type": "string",
                    "pattern": "^(messages|hours):[1-9][0-9]{0,2}$",
                    "example": "hours:48",
                    "description": "Room scope only: `messages:<n>` (n up to 200) or `hours:<h>` (h up to 168; at most 200 messages). Out of bounds is refused, never clamped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued — the summary arrives as a room message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "`feature_disabled` (either setting off); `forbidden` — not a room member; `im_not_allowed` — may not post (or reply in this channel thread); `information_barrier` in a DM the company's information barriers keep apart; or Chat app disabled"
          },
          "404": {
            "description": "`room_not_found` / `thread_root_not_found` (not in this room, deleted, or a reply rather than a root)"
          },
          "422": {
            "description": "`invalid_window`"
          },
          "429": {
            "description": "`rate_limited` (5 per room per hour) / `budget_exhausted` (the company's monthly Ask AI budget is spent)"
          },
          "503": {
            "description": "`entitlement_unavailable` — the plan could not be checked just now; try again"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/ask_ai/suggest_replies": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Smart reply suggestions for one message (private to the caller)",
        "description": "Up to three short replies the caller could send to `message_id`,\ngenerated now and returned to the caller alone — nothing is posted or\nstored. A tapped suggestion should only fill the composer, never send.\nA message that gets no suggestions — the caller's own, the\nassistant's, a room notice, one with no text, or a read-receipt request\nthe caller has not acknowledged — answers 200 with an empty list and\ncosts nothing from the hourly allowance. A model outage or malformed completion\nanswers 503 service_unavailable and refunds the hourly slot. Valid [] answers 200.\n60 per person per hour. Needs the Chat settings `ask_ai_enabled` and\n`ask_ai_smart_replies_enabled`, and Platform Standard or above. A POST,\nso a token needs write:chat though nothing is written.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message_id"
                ],
                "properties": {
                  "message_id": {
                    "type": "integer",
                    "description": "A live message in this room."
                  },
                  "context_scope": {
                    "type": "string",
                    "enum": [
                      "room",
                      "thread"
                    ],
                    "default": "room"
                  },
                  "thread_root_id": {
                    "type": "integer",
                    "description": "Required for thread scope; must contain message_id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Zero to three suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "maxItems": 3,
                      "items": {
                        "type": "string",
                        "maxLength": 200
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "`feature_disabled` (either setting off); `forbidden` — not a room member; or Chat app disabled / no write:chat"
          },
          "404": {
            "description": "`room_not_found` / `message_not_found` (not in this room, or deleted)"
          },
          "422": {
            "description": "`invalid_request` — no `message_id`"
          },
          "429": {
            "description": "`rate_limited` (60 per person per hour) / `budget_exhausted` (the company's monthly Ask AI budget is spent)"
          },
          "503": {
            "description": "`service_unavailable` — model unavailable or invalid output; `entitlement_unavailable` — plan check unavailable"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/remind": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Remind the people who have not acknowledged your message",
        "description": "The sender only, on their Important or read-receipt message: a bell\nrow and a push (no email) to everyone still to acknowledge it, under\ntheir own notification settings (a muted room, \"Nothing\" for the room\nor Do Not Disturb drops the push, not the bell row). People whose read\nstatus a works council withholds are left out. Only for a message\nrecorded as sent by a manager or room admin, never in a one-to-one\nchat, and at most once in 24 hours (automatic reminders count). GET\n…/acknowledgements says in advance (`can_remind`,\n`remind_unavailable_reason`). (`remind`, not `reminder`: …/reminder\nsets a reminder for the caller.)\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": "Reminder sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "message_id": {
                      "type": "integer"
                    },
                    "recipients": {
                      "type": "integer",
                      "description": "People the reminder reached."
                    },
                    "reminder_count": {
                      "type": "integer",
                      "description": "Reminders sent for this message so far",
                      "automatic and manual.": null
                    },
                    "last_reminded_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`forbidden` — not a room member, or not the sender; or Chat app disabled"
          },
          "404": {
            "description": "`room_not_found` / `message_not_found` (or deleted)"
          },
          "422": {
            "description": "`direct_chat` (a one-to-one chat); `not_remindable` (a message\nthat asks for no acknowledgement, a deleted one, a room notice, one\nnot recorded as a manager's or room admin's, or a sender no longer\nactive here); `feature_off` (Important messages / Read receipt\nrequests turned off); `too_soon` (reminded in the last 24 hours);\n`nobody_pending`; `nobody_reachable` (everyone pending has these\nnotices off); `not_delivered` (no reminder landed — try again).\nNothing is counted on any of them.\n"
          }
        }
      }
    },
    "/chat/rooms/{room_id}/messages/{message_id}/translate": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Translate a message for the caller",
        "description": "Translates the message's text into `lang`, or into the caller's own\ncontent language (their profile language, else their location's, else\nthe company's default, else English). Cached per message and language,\nso other readers in that language reuse it. A message already in the\ntarget language answers `translated: false` with no `body`. Not a\nread-receipt request the caller has not acknowledged. 30 fresh\ntranslations per person per hour — one answered from the cache (asked\nagain, or already translated for another reader in that language) is\nnot counted and is served even past the limit. Off when the Chat\nsetting `message_translation_enabled` is off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lang": {
                    "type": "string",
                    "example": "es",
                    "description": "A language the translator supports, in any case; a region variant folds to its base language (en-US → en) unless it is supported itself (pt-BR, zh-TW). Omit for the caller's own."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Translated, or already in that language",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "message_id": {
                      "type": "integer"
                    },
                    "language": {
                      "type": "string",
                      "description": "The target language used."
                    },
                    "provider": {
                      "type": "string",
                      "description": "Omitted when unknown."
                    },
                    "translated": {
                      "type": "boolean",
                      "description": "False when the message was already in `language`."
                    },
                    "source_language": {
                      "type": "string",
                      "description": "The detected language of the message. Omitted when unknown."
                    },
                    "body": {
                      "type": "string",
                      "description": "The translated text — present only when `translated` is true."
                    },
                    "message": {
                      "type": "string",
                      "description": "Present only when `translated` is false: the sentence saying why, in the caller's language — \"This message is already in your language.\" (Nothing to translate and translation being off are refusals: `nothing_to_translate` 422 and `feature_disabled` 403, each with its sentence in `error.message`.)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "`feature_disabled` (answered before the room is looked up); `forbidden` — not a room member; or Chat app disabled"
          },
          "404": {
            "description": "`room_not_found` / `message_not_found` (or deleted)"
          },
          "422": {
            "description": "`nothing_to_translate` (no text), `ack_required` (an unacknowledged read-receipt request), `unsupported_language`, or `translation_failed` (the provider failed — this one also carries `ok: false`)"
          },
          "429": {
            "description": "`rate_limited` (30 fresh translations per person per hour)"
          }
        }
      }
    },
    "/chat/ask_ai/actions/{id}/confirm": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Confirm an action the assistant proposed in a room",
        "description": "`id` is `ai_affordances.action_request.id` on the assistant's message.\nOnly someone in its `permitted_confirmer_ids` may resolve it — the\nperson who asked, or for a proactive suggestion the people it is\nabout. Confirming runs the paused action now and posts the assistant's\noutcome to the room before answering: a 200 says the action was\nconfirmed, not that it succeeded — that is in the assistant's message.\nRefused, and left pending, when that assistant is no longer on for the\ncaller or the company's work-context policy closes its app to them now.\nEvery resolution is broadcast as `ask_ai:action_resolved` on\n`private-room-<id>` ({ room_id, message_id, action_request }). An\naction not resolved within 15 minutes expires. Needs the Chat setting\n`ask_ai_actions_enabled` and Platform Standard or above.\n\nThe confirmed tools run under the request's token: a tool its scopes\ndo not grant is refused (the assistant's outcome message says so).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmed — the terminal action request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "`feature_disabled`; `not_permitted` — not in `permitted_confirmer_ids`; `forbidden` — that assistant is no longer on for the caller, or the work-context policy closes its app now (the body also carries `action_request`); or Chat app disabled"
          },
          "404": {
            "description": "`action_not_found`"
          },
          "409": {
            "description": "`already_resolved`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The house error envelope plus the action as it now stands, so the row can be updated in place.",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "`expired` — including a pending action whose paused turn can no longer be resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The house error envelope plus the action as it now stands, so the row can be updated in place.",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`service_unavailable` — the work-context policy could not be checked (the body also carries `action_request`); `entitlement_unavailable` — the plan could not be checked. Try again."
          }
        }
      }
    },
    "/chat/ask_ai/actions/{id}/cancel": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Decline an action the assistant proposed in a room",
        "description": "Same rule as confirm: `permitted_confirmer_ids` only. Marks the action\ncancelled and drops the paused turn — nothing runs. Broadcast as\n`ask_ai:action_resolved`. Needs the Chat setting\n`ask_ai_actions_enabled` and Platform Standard or above.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled — the terminal action request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "`feature_disabled`; `not_permitted` — not in `permitted_confirmer_ids`; or Chat app disabled"
          },
          "404": {
            "description": "`action_not_found`"
          },
          "409": {
            "description": "`already_resolved`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The house error envelope plus the action as it now stands, so the row can be updated in place.",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "`expired`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The house error envelope plus the action as it now stands, so the row can be updated in place.",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "action_request": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "create_task",
                            "create_calendar_event",
                            "schedule_huddle",
                            "create_ticket",
                            "generic"
                          ],
                          "description": "Render an unknown value as generic."
                        },
                        "summary": {
                          "type": "string",
                          "example": "Create task \"Fix login bug\" assigned to Priya"
                        },
                        "invoker_user_id": {
                          "type": "integer"
                        },
                        "permitted_confirmer_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "confirmed",
                            "cancelled",
                            "expired"
                          ]
                        },
                        "resolved_by_user_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`entitlement_unavailable` — the plan could not be checked just now; try again"
          }
        }
      }
    },
    "/chat/ask_ai/search": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Ask a question of your own chat conversations",
        "description": "Answers a question from the rooms the caller is in, and only those,\nwith the messages it rests on — now, to the caller alone; the question\nis not saved to their Ask AI history. `answer` is null with no\ncitations when nothing in their conversations answers it. An `[n]`\nmarker in `answer` points at `citations[n-1]`. The question passes the\ncompany's content moderation first. 30 per person per hour. Needs the\nChat settings `ask_ai_enabled`, `ask_ai_retrieval_enabled` and\n`semantic_search_enabled`, and Platform Standard or above. A POST, so a\ntoken needs write:chat though nothing is written.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query"
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer and its sources, or null when nothing grounds",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answer": {
                      "type": "string",
                      "nullable": true
                    },
                    "citations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "integer"
                          },
                          "message_id": {
                            "type": "integer"
                          },
                          "text": {
                            "type": "string",
                            "maxLength": 200,
                            "description": "An excerpt of the message."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "Platform tier below Standard (error.code \"upgrade_required\")"
          },
          "403": {
            "description": "`feature_disabled` (any of the three settings off); or Chat app disabled / no write:chat"
          },
          "422": {
            "description": "`invalid_request` (blank query) / `query_too_long` (over 500\ncharacters); or refused by content moderation, in a flat body —\n`{ \"error\": \"<sentence>\", \"blocked\": true, \"reason\": \"…\" }`.\n"
          },
          "429": {
            "description": "`rate_limited` (30 per person per hour) / `budget_exhausted` (the company's monthly Ask AI budget is spent)"
          },
          "503": {
            "description": "`entitlement_unavailable` — the plan could not be checked just now; try again"
          }
        }
      }
    },
    "/chat/presence/offline": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Mark the caller offline now",
        "description": "The sign-out / app-quit signal: drops the caller's presence so others\nsee them offline on their next GET /chat/presence/online, instead of\nafter the 5-minute heartbeat expiry. The next heartbeat marks them\nonline again. Idempotent — safe to send on both sign-out and window\nclose.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Offline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "offline"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/reactions": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "The quick reaction set",
        "description": "The reactions to draw the picker from, instead of a hard-coded list:\nthe company's reaction palette (an admin's choice in Communications\nsettings, used in every app; the whole catalog when none was chosen).\nThe same list is `reactions` in GET /chat/config. POST …/reactions also\ntakes any single emoji (`reactions_accept_any_emoji`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The palette, in display order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "emoji": {
                            "type": "string",
                            "example": "👍"
                          },
                          "key": {
                            "type": "string",
                            "nullable": true,
                            "example": "like",
                            "description": "The reaction's catalog key; null for a glyph outside the catalog."
                          },
                          "label": {
                            "type": "string",
                            "example": "Like",
                            "description": "For tooltips and screen readers."
                          },
                          "icon_url": {
                            "type": "string",
                            "nullable": true,
                            "example": "https://acme.workforce.mangoapps.com/assets/reactions/like-1a2b3c.svg",
                            "description": "Absolute URL of the drawn icon the web shows for this reaction. null ⇒ draw the emoji glyph."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled"
          }
        }
      }
    },
    "/chat/deleted_rooms": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Conversations the caller may restore after \"Delete for everyone\"",
        "description": "The API twin of the web Chat › Recently deleted page, and the list behind `POST /chat/deleted_rooms/{id}/restore`. A business admin or Chat app admin — with a token that may act as one (`write:chat` / `read:chat`, not an `own_chat` grant) — sees every conversation of the company that was deleted for everyone; anyone else sees only the ones they deleted (`scope` says which). Newest deleted first, 25 a page. A conversation can be restored until `restorable_until` (`restore_window_days` after it was deleted, a fixed length: the same instant for every viewer and for the nightly purge, across a clock change too). One on a legal hold is never purged, so it stays restorable after that. Holds are told to business (company) admins only, as the room payload's `on_legal_hold` is: for them `legal_hold` is true and `restorable_until` null while a hold keeps it. Anyone else — a Chat app admin who is not a company admin included — gets `legal_hold: null` and the window's end, so a held row past its window still has `can_restore: true` after `restorable_until`: show it as due any day (\"Any day now\"), with Restore still offered, as the web page does. Branch on `can_restore`, never on the clock.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of deleted conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted_rooms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "display_name": {
                            "type": "string",
                            "description": "The title to show, never null, as the web's Recently deleted page names it: `name` when set; else, where the caller may see who was in it (as `participants`), a group's first three people and how many more (\"Ann, Bo, Cy and 2 more\") or a direct message's two (\"Ann & Bo\"); else the words every list uses — \"Untitled group\", \"Untitled channel\" or \"Direct message\" (the room payload's `display_name` fallback). In the caller's language."
                          },
                          "room_type": {
                            "type": "string",
                            "enum": [
                              "group",
                              "channel"
                            ]
                          },
                          "is_public": {
                            "type": "boolean",
                            "description": "The room payload's `is_public`: true for a public channel (listed in Browse Channels, anyone may join), false for a private channel and every group. Draw a private channel with its lock, as the web's Recently deleted page does."
                          },
                          "member_count": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Members still in it. Null where the caller may not see who is in it (its \"Who can see members?\" is admins only and they are not a room admin there) — never on the company list."
                          },
                          "participants": {
                            "type": "array",
                            "maxItems": 3,
                            "description": "The first three people still in it, in roster order — name an unnamed conversation by them (\"Ann, Bo, Cy and 2 more\" with `member_count`), as the web page does. Empty where `member_count` is null.",
                            "items": {
                              "$ref": "#/components/schemas/ChatUserSummary"
                            }
                          },
                          "discarded_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "discarded_by": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "legal_hold": {
                            "type": "boolean",
                            "nullable": true,
                            "description": "True while a legal hold keeps it from being purged. Business (company) admins only; null for everyone else, who are not told a hold exists."
                          },
                          "restorable_until": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "The end of the restore window. Null while a legal hold keeps it — told to company admins only; anyone else always gets the window's end."
                          },
                          "can_restore": {
                            "type": "boolean",
                            "description": "POST /chat/deleted_rooms/{id}/restore would restore it now: false once it is past its window and no hold keeps it (it is being permanently deleted — show \"Can't be restored\"), and for a token without write:chat — or, on the company list, for someone else's deletion when the token holds only write:own_chat (restore then finds only the caller's own). Can be true after `restorable_until` (a hold the caller is not told of keeps it)."
                          }
                        }
                      }
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "company",
                        "own"
                      ],
                      "description": "`company` — every conversation the company deleted for everyone (a business or Chat app admin, under a token that may act as one); `own` — only the ones the caller deleted. Say which in the list's subtitle."
                    },
                    "restore_window_days": {
                      "type": "integer",
                      "example": 30,
                      "description": "How long a deleted conversation stays restorable — the constant the purge and the restore refusal count from."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "has_more": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled",
            "or token lacks read:chat": null
          }
        }
      }
    },
    "/chat/deleted_rooms/{id}/restore": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Restore a conversation deleted for everyone",
        "description": "The undo of `DELETE /chat/rooms/{id}?for_all=true`, on the same rules as `GET /chat/deleted_rooms` (a Chat admin any of the company's, anyone else their own). The conversation comes back with its full history and members; its members get a `room:restored` event and a bell notice. A channel whose name another live channel took meanwhile comes back named `<name>-<id>`. Refused once its restore window has passed and no legal hold keeps it (it is being permanently deleted) — the row's `can_restore` says so beforehand.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Restored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "integer"
                    },
                    "restored": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Chat app disabled",
            "or token lacks write:chat": null
          },
          "404": {
            "description": "`room_not_found` — not in the caller's Recently deleted (never deleted, already restored, purged, or someone else's deletion and the caller is not a Chat admin)"
          },
          "422": {
            "description": "`restore_window_passed`, or `not_restored` (it could not be saved; still in Recently deleted — try again)"
          }
        }
      }
    },
    "/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 marked reviewed (or, if never reviewed, its question or answer text not changed) within the workspace's staleness window, 12 months by default) 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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RfpAnswer"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "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.\n\nSetting `active` to false on a live answer retires it as\n`POST /rfp_desk/answers/{id}/archive` does, so that update is\ndelete-level (owner decision 2026-10-04, #6721): on top of\n`write:rfp_response` it requires the `destructive:rfp_response` scope.\n",
        "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 — restarts its staleness window (12 months unless the tenant set another).",
        "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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:rfp_response` scope.\n",
        "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RfpResponseSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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
                  },
                  "base_draft_version": {
                    "type": "string",
                    "description": "Optional. The section's `draft_version` (a digest of the answer's words, returned on every section) the caller read the answer on. When the stored answer has changed since and this call would replace it with different words, nothing is written and the call answers 409 `draft_conflict`, whose `details` carry the saved `draft` and its `draft_version`. Omit it to write over whatever is stored."
                  }
                }
              }
            }
          }
        },
        "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
                    },
                    "withdrawn_sign_offs": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Specialist sign-offs (e.g. \"Legal\") this change withdrew; they have to be given again"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Either `response_locked` (the response is approved or submitted — reopen it in RFP Desk first) or `draft_conflict` (the answer was saved by someone else since `base_draft_version`; `error.details` holds `draft` and `draft_version` as saved)."
          },
          "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); anyone may while nobody is responsible. Approval needs every section answered and reviewed, the specialist sign-off on every Legal, Security, Commercial or HR section, and every required deliverable ticked (422 with the reason otherwise). Declining withdraws routed questions from people's task lists.\n\nDeclining a bid (`status` `declined`) is delete-level (owner decision\n2026-10-04, #6721): requires the `destructive:rfp_response` scope.\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"
                    },
                    "questions_to_resend": {
                      "type": "integer",
                      "description": "On a reopen of a declined bid (declined to draft): how many routed questions the decline withdrew that are still unanswered and not back in anyone's task list — reopening does not restore them, so send each to its expert again. 0 on every other move."
                    },
                    "cleared_outcome": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "won",
                        "lost",
                        null
                      ],
                      "description": "The Won/Lost outcome a reopen (submitted to draft) cleared, or null. A reopen of a bid whose outcome comes from its Mango GTM proposal is refused with 409 outcome_owned_by_proposal."
                    }
                  }
                }
              }
            }
          },
          "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
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Request conflicts with current state",
            "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": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CollectionEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RfpAssessmentSummary"
                          }
                        },
                        "meta": {
                          "$ref": "#/components/schemas/CollectionPaginationMeta"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          }
        }
      }
    },
    "/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,
                    "description": "One of the tenant's active note templates (`GET /ai_notepad/note_templates`). Links it to the note: a recording, upload or pasted transcript takes the template's section headings when processing completes, and a typed note's first enhance runs under it. Not a body pre-fill. 422 `template_not_found` for any other id."
                  },
                  "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."
                  },
                  "occurred_at_edge": {
                    "type": "string",
                    "enum": [
                      "start",
                      "end"
                    ],
                    "description": "Which end of the conversation `occurred_at` is, so the note is named after the calendar meeting that was running then: `end` for the moment a capture stopped, `start` for when it began. Defaults to `end` when `occurred_at` is absent and `duration_sec` is sent; otherwise the server infers it, which is wrong for a capture uploaded later than it is long. Any other value is a 422 `invalid_occurred_at_edge`."
                  },
                  "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/upcoming/dismiss": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Answer the pre-meeting pop-up with \"Not now\"",
        "description": "Remembers, for the caller and one calendar occurrence, that the pop-up\nwas answered. The web prompt and the phone sheet read the same list, so\nnone of them offers it again, and the next `GET /meetings/upcoming`\nmarks the row `dismissed: true`. Repeating it changes nothing.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "calendar_event_id"
                ],
                "properties": {
                  "calendar_event_id": {
                    "type": "string",
                    "description": "The key `GET /meetings/upcoming` returned."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remembered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dismissed": {
                      "type": "boolean"
                    },
                    "calendar_event_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          },
          "422": {
            "description": "The key is not a calendar occurrence key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/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\nDelete-level (owner decision 2026-10-04): requires the `destructive:ai_notepad`\nscope.\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 creator or someone it was shared with directly as an editor or 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 creator or someone it was shared with directly as an editor or 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}/workspace": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Link a meeting to a workspace, move the link, or remove it",
        "description": "A linked note's action items become that workspace's to-dos when tasks\nare created from them. Linking also posts the note's summary to the\nworkspace's message board — only when every active member of the\nworkspace can already open the note, and never on a workspace anyone\nin the organization can join or preview; `summary.message` says what\nhappened, in the sentence the web shows. A blank / null `workspace_id`\nremoves the link (to-dos already created there stay). The target must be\nan active workspace the caller is a member of with a role that can add\nto-dos (not a viewer); anything else answers 404 `workspace_not_found`.\nOnly the note's creator or someone it was shared with directly as an\neditor or owner may change the link (403).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "workspace_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Null or blank removes the link."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated meeting, and what happened to its summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingEntity"
                    },
                    "summary": {
                      "type": "object",
                      "nullable": true,
                      "description": "Null when the link was removed.",
                      "properties": {
                        "posted": {
                          "type": "boolean"
                        },
                        "outcome": {
                          "type": "string",
                          "description": "posted, pending_review or already_posted when posted; otherwise no_summary, not_visible, refused or not_linkable."
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only the note's creator or someone it was shared with directly as an editor or owner can change which workspace it's linked to."
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadValidationError"
          }
        }
      }
    },
    "/ai_notepad/meetings/linkable_accounts": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Accounts a note can be linked to",
        "description": "The business's Mango CS accounts the caller may link a note to — the ones they own (CSM or AE) come first with `mine: true`, the same \"My accounts / All accounts\" split the web picker shows. `q` narrows by name or domain; at most 50 rows are returned, so search rather than scroll. 403 `mango_cs_unavailable` when the Mango CS app is off for the business or not available to the caller — hide the account controls on that.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "river"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The linkable accounts, the caller's own first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "domain": {
                            "type": "string",
                            "nullable": true
                          },
                          "mine": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "q": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`mango_cs_unavailable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/account": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Link the note to a Mango CS account",
        "description": "The desktop half of \"assign an account to a note\": links the note to `crm_account_id` (the web's \"Link to an account\") and answers the refreshed meeting with `account` set, so the client redraws without a refetch. Editors of the note only; the note must not be in Recently Deleted. Same Mango CS gate as the picker.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "crm_account_id"
                ],
                "properties": {
                  "crm_account_id": {
                    "type": "integer",
                    "example": 142
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The refreshed meeting (detail shape) with `account` set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (not an editor) or `mango_cs_unavailable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "The note, or the account (`account_not_found`), is not there"
          }
        }
      },
      "delete": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Unlink the note from its account",
        "description": "The web's \"Unlink\". 422 `meeting_not_linked` when there is nothing to unlink. Answers the refreshed meeting with `account: null`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The refreshed meeting with `account` cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` or `mango_cs_unavailable`"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "description": "`meeting_not_linked`"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/account/suggestion/dismiss": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Dismiss the note's customer suggestion",
        "description": "\"Not this customer\" on the detail payload's `account_suggestion` (finding #4182): Mango CS's automatic linker only suggests the customer a note concerns, and its owner confirms (POST /ai_notepad/meetings/{id}/account with the suggested `crm_account_id`) or dismisses it here. A dismissed account is never suggested for this note again. Same right as confirming; the note must not be in Recently Deleted. Answers the refreshed meeting with `account_suggestion: null`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The refreshed meeting with `account_suggestion` cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeetingDetail"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (not the note's creator or a direct owner, or a direct editor while “Editors can pass a note on” is on), `meeting_deleted` or `mango_cs_unavailable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "The note is not there, or it carries no suggestion (`suggestion_not_found`)"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/reshape": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Reshape the AI notes under a note template",
        "description": "The API twin of the web's Enhance ▾ → \"Reshape the AI notes\". `\"auto\"` lets the model pick the sections (and clears the note's template link); a template id makes that template's section headings the insight sections and the summary's structure, and the note remembers it (`note_template` on the detail). Open to every source with text — a recording's transcript as much as a typed note. Rewrites the summary and the insights only; action items are left alone. Returns 202 with the meeting back in `processing`; completion arrives via the realtime `artifacts:ready` event, exactly like regenerate. Templates come from `GET /ai_notepad/note_templates`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note_template_id"
                ],
                "properties": {
                  "note_template_id": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "auto"
                        ]
                      },
                      {
                        "type": "integer"
                      }
                    ],
                    "description": "`\"auto\"`, or the id of one of the tenant's active note templates."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Reshape enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    },
                    "note_template": {
                      "type": "object",
                      "nullable": true,
                      "description": "The template the notes are being reshaped to; null for `auto`.",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an editor, the note is in Recently Deleted, or AI processing is disabled by the administrator (`ai_processing_disabled`)."
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "409": {
            "description": "`already_processing` — the note is being processed right now."
          },
          "422": {
            "description": "`note_template_id_required`, `template_not_found` (not one of the tenant's active templates), `nothing_to_reshape` (no text yet), or `invalid_transition`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "The AI pipeline could not be queued (`processing_unavailable`). The note is marked `failed`; repeat the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/note_templates": {
      "get": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "The tenant's note templates",
        "description": "The same `active` set the web's pickers offer — every id here is one `POST /meetings` (`template_id`), `POST /meetings/{id}/reshape` and `POST /meetings/{id}/section/revise` (`revision_action: template`) accept. `sections` are the headings the AI notes take under the template. `default_template_id` is the effective default for the caller (their own preference over the admin's), spelled as GET /config spells it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Templates, by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotepadNoteTemplate"
                      }
                    },
                    "default_template_id": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadForbidden"
          }
        }
      }
    },
    "/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}/speakers/{speaker_id}": {
      "patch": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Name a transcript speaker",
        "description": "Names one speaker (\"Speaker A\" → the person) for every reader; a blank `name` removes the name and the roster's own label returns. Editors only; refused on a note in Recently Deleted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "$ref": "#/components/parameters/NotepadSpeakerId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Named. The roster, the live suggestions and whether the notes now predate a name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSpeakerNaming"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadError"
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "422": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/speakers/{speaker_id}/suggestion/accept": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Accept a \"Who is Speaker A?\" suggestion",
        "description": "Names the speaker the suggestion proposed, through the same store the rename writes. Never over a name somebody typed since — that answers 409 `speaker_already_named`. Editors only; refused on a note in Recently Deleted (403 `meeting_deleted`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "$ref": "#/components/parameters/NotepadSpeakerId"
          }
        ],
        "responses": {
          "200": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSpeakerNaming"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadError"
          },
          "404": {
            "description": "No such note, or no pending suggestion for that speaker (`suggestion_not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/NotepadError"
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/speakers/{speaker_id}/suggestion/dismiss": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Dismiss a \"Who is Speaker A?\" suggestion",
        "description": "Retires the suggestion for this transcript; it is not offered again. Editors only; refused on a note in Recently Deleted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          },
          {
            "$ref": "#/components/parameters/NotepadSpeakerId"
          }
        ],
        "responses": {
          "200": {
            "description": "Dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSpeakerNaming"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotepadError"
          },
          "404": {
            "description": "No such note, or no pending suggestion for that speaker (`suggestion_not_found`).",
            "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
                      }
                    },
                    "speaker_suggestions": {
                      "type": "array",
                      "description": "\"Who is Speaker A?\" — the same live suggestions the detail payload carries, beside the roster they name.",
                      "items": {
                        "$ref": "#/components/schemas/NotepadSpeakerSuggestion"
                      }
                    },
                    "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"
          },
          "503": {
            "description": "The AI pipeline could not be queued (`processing_unavailable`). The note is marked `failed`; repeat the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          },
          "503": {
            "description": "The AI pipeline could not be queued (`processing_unavailable`). The note is marked `failed`; repeat the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/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": "Text saved. `processing.started` says whether the pipeline was enqueued; `reason: processing_unavailable` means it could not be, and the note is then `failed` (reprocess it with `regenerate`).",
            "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"
          },
          "502": {
            "description": "File storage refused the recording (`upload_storage_unavailable`). Nothing was changed on the note; send the file again. An infected file answers 422 `upload_blocked`; nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "The recording is attached, but the AI pipeline could not be queued (`processing_unavailable`). The note is marked `failed`; repeat the request, or reprocess it with `POST /ai_notepad/meetings/{id}/regenerate`. Also `scanner_unavailable`: virus scanning is down and this workspace refuses uploads meanwhile; nothing was stored, so send the file again in a few minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/ai_notepad/meetings/{id}/audio/init": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Begin a presigned recording upload (deprecated)",
        "deprecated": true,
        "description": "DEPRECATED — use Upload Sessions (`POST /uploads/sessions` with `purpose: \"ai_notepad\"`), which sends the file through the API in resumable parts and works on every storage provider, then finalize with `audio/complete` exactly as before. This leg returns a presigned direct-upload target the client PUTs the file to; it remains for clients already in the field and will be removed once they have moved.",
        "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 (or an upload session's hand-off). The id `audio/init` returns is accepted only here — no other endpoint that takes a signed blob id will."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Attached and pipeline enqueued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meeting": {
                      "$ref": "#/components/schemas/NotepadMeeting"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`ai_processing_disabled` — an admin has switched AI Notepad's processing off for this workspace; nothing was attached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "413": {
            "description": "Larger than the tenant's upload ceiling (as declared at `audio/init`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported audio format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "Refused, nothing attached. `upload_blocked` — the virus scan found malware in the uploaded file, which is deleted (final; do not retry). `upload_incomplete` — the PUT never reached storage, or the stored bytes do not match the checksum declared at `audio/init`; run `audio/init` and upload again. Other codes as `NotepadError`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "503": {
            "description": "Three causes, told apart by `error.code`. `scanner_unavailable` — uploads are paused while virus scanning is unavailable; nothing was attached and the uploaded file is kept, so repeat this request in a few minutes. `storage_unavailable` — file storage could not be read for the scan; nothing was attached and the uploaded file is kept, so repeat this request. `processing_unavailable` — the recording is attached, but the AI pipeline could not be queued. The note is marked `failed`; reprocess it with `POST /ai_notepad/meetings/{id}/regenerate` (repeating this request with the same `signed_blob_id` works only for the note's creator, because the blob is now attached).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/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": "Hands the section's current content back to the model and saves the result — the web's AI Actions menu. Responds with the same body as a section save. Synchronous: the model is called inline. Gated on the tenant's `content_revision_enabled` toggle and the AI kill switch (403 `content_revision_disabled` / `ai_processing_disabled`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/NotepadMeetingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "revision_action"
                ],
                "properties": {
                  "section": {
                    "$ref": "#/components/schemas/NotepadSectionName"
                  },
                  "revision_action": {
                    "type": "string",
                    "enum": [
                      "rewrite",
                      "expand",
                      "refine",
                      "simplify",
                      "custom",
                      "template"
                    ],
                    "description": "`custom` takes the whole directive in `instructions` (required for it); `template` reshapes the SUMMARY under a note template's headings and takes `template_id` (summary only — refused on the schema-locked sections)."
                  },
                  "instructions": {
                    "type": "string",
                    "description": "Free-text steer — optional for the presets, required for `custom`."
                  },
                  "template_id": {
                    "type": "integer",
                    "description": "Required for `template` — one of the tenant's active note templates (`GET /ai_notepad/note_templates`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The revised section",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadSectionSaveResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotepadNotFound"
          },
          "409": {
            "description": "`section_changed_during_revision` — the section was changed (an item ticked, a version restored, another edit) while the model was rewriting it, so nothing was replaced. Re-read the section and call again to revise the current version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "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, or on the caller's notes, 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. With `from=my_notes` it lists instead the items on notes the caller wrote, whoever they are assigned to — each row's `assignee` names that person (My Items' \"From my notes\" list).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "assignee",
            "in": "query",
            "description": "Only `me` is supported. Leave it out with `from=my_notes`.",
            "schema": {
              "type": "string",
              "enum": [
                "me"
              ],
              "default": "me"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Whose items: `assigned` (the items assigned to the caller) or `my_notes` (the items on notes the caller wrote). Any other value is a 422 `unsupported_list`.",
            "schema": {
              "type": "string",
              "enum": [
                "assigned",
                "my_notes"
              ],
              "default": "assigned"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "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) or sets `status` directly. An assignee change emits the realtime `action_item:assigned` event.\n**Required Scopes:** `write:ai_notepad`, and also `destructive:ai_notepad` when `status` is `cancelled` — cancelling an action item is a final act (owner decision 2026-10-04, #6721); a token without it is refused with 403 `insufficient_permissions` (\"Required scope: destructive:ai_notepad\"). Done or back to pending needs only the write scope.",
        "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"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "completed",
                      "cancelled"
                    ],
                    "description": "Wins over `done` when both are sent; `cancelled` also needs `destructive:ai_notepad`."
                  },
                  "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. Same scopes: `write:ai_notepad`, and also `destructive:ai_notepad` when `status` is `cancelled`.",
        "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"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "completed",
                      "cancelled"
                    ],
                    "description": "Wins over `done` when both are sent; `cancelled` also needs `destructive:ai_notepad`."
                  },
                  "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",
        "description": "Delete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:ai_notepad` scope.\n",
        "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.\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:ai_notepad` scope.\n",
        "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, interleaved with `: keepalive` comment lines (about every 10s, after the first real frame) that keep the connection from being torn down while idle. Readers must skip any line beginning with `:` — it is an SSE comment, not an event.",
            "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, interleaved with `: keepalive` comment lines (about every 10s, after the first real frame) that keep the connection from being torn down while idle. Readers must skip any line beginning with `:` — it is an SSE comment, not an event.",
            "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/transcription/tickets": {
      "post": {
        "tags": [
          "AI Notepad"
        ],
        "summary": "Mint a cloud transcription ticket for one capture",
        "description": "The desktop asks for a ticket once per capture and presents it to the\ntranscription relay, which holds the provider key — the client never\nsees it. The ticket is a signed token that expires after 5 minutes;\nthe relay checks it when the stream opens.\n\nEvery refusal carries a code the desktop turns into an on-device\ncapture, checked in this order: capture is off for the organization\n(`capture_disabled_by_policy`), the caller is outside the who-may-record\npolicy (`capture_not_permitted`), the organization transcribes on the\ndevice (`cloud_not_in_effect`, with `reason: provider_unavailable` when\nthis environment has no provider key), the relay is not configured\nhere (`relay_not_configured`), or the AI Voice credits or the monthly\ntranscription budget are used up (`wallet_empty`, 402 — the message\nsays which).\n\n**Token scopes:** `write:ai_notepad`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string",
                    "description": "The capture's own id, chosen by the client; bound into the ticket."
                  },
                  "language": {
                    "type": "string",
                    "maxLength": 35,
                    "description": "The spoken language hint, cut to 35 characters."
                  },
                  "keywords": {
                    "type": "array",
                    "maxItems": 64,
                    "description": "Words the provider should listen for. Blanks and duplicates are dropped; at most 64 are kept.",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket and the relay to present it to.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ticket",
                    "expires_at",
                    "provider"
                  ],
                  "properties": {
                    "ticket": {
                      "type": "string",
                      "description": "The signed ticket. Never log it."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Five minutes after it was minted."
                    },
                    "relay": {
                      "type": "object",
                      "nullable": true,
                      "description": "Where to open the stream. Null when the environment's relay origin does not resolve to a `wss://` address.",
                      "properties": {
                        "url": {
                          "type": "string",
                          "example": "wss://relay.example.com/api/v1/ai_notepad/transcription/stream"
                        },
                        "protocol": {
                          "type": "integer",
                          "example": 1
                        }
                      }
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "meta_muse"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`wallet_empty` — the organization's credits or this month's transcription budget are used up; capture on the device.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:ai_notepad\"), `app_not_enabled`, `app_not_accessible`, `capture_disabled_by_policy`, `capture_not_permitted`, `cloud_not_in_effect` (with a `reason`) or `relay_not_configured`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotepadErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "`session_id_required` — `session_id` is missing or blank.",
            "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` (AI Notepad), `chat_not_enabled` /\n`chat_not_accessible` (the Chat app is off or not open to the\ncaller), 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": "By default every row is a `notebook_id` both write paths accept — created by the caller, or shared with them as editor/owner. A client whose sidebar is navigation sends `include=readable` to also receive the notebooks shared with the caller as Viewer, each with `can_file: false`, so a filing picker can keep excluding them.",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "`readable` adds the view-only shares to the list. Omit it for the filing set.",
            "schema": {
              "type": "string",
              "enum": [
                "readable"
              ]
            }
          }
        ],
        "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.\nDelete-level (owner decision 2026-10-04, #6721): requires the `destructive:ai_notepad` scope.",
        "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"
          }
        }
      }
    },
    "/people/changes/{id}": {
      "get": {
        "tags": [
          "People Changes"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fetch one approved profile change",
        "description": "The change a write-back webhook referenced (its `fetch_url`), rebuilt\nfrom the approved request and the person's record at fetch time — the\nvalues are the person's CURRENT values for the attributes the change\nwrites. Nothing is stored on the ledger row, so a receiver that fetches\nlate gets today's value.\n\nRequires an administrator's token with `read:people_changes`.\n\nA signed record sent to the same webhook (event type\n`people.compliance_record` — a signed policy acknowledgment, INT-HR-5)\nis fetched and confirmed here too: its `record_type` is\n`compliance_record`, `change_type` is `policy_attestation`, `role` is\n`copy`, and `values` carries `attestation_id`, `policy_id`,\n`policy_title`, `policy_version`, `signed_at`, `cycle_kind`, `door`,\n`signature_method` and `signed_document_sha256`.\n\nAnswers 410 for a change that must not be applied now — anything but\nin flight or confirmed, for example superseded by a newer change to\nthe same field, discarded, failed or conflicted; `error.details.status`\nnames its state.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The change id from the webhook's `change_id`.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The change, rebuilt now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "change": {
                      "$ref": "#/components/schemas/PeopleChange"
                    }
                  }
                },
                "example": {
                  "change": {
                    "change_id": 812,
                    "change_type": "phone",
                    "label": "Phone Number",
                    "role": "owner",
                    "subject": {
                      "user_id": 4521,
                      "employee_id": "E-10042",
                      "email": "jordan@example.com"
                    },
                    "values": {
                      "phone": "+1 (555) 555-0142"
                    },
                    "effective_date": null,
                    "approved_at": "2026-09-24T15:02:11Z",
                    "sensitive": false,
                    "status": "sent"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks read:people_changes, or its user is not an administrator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "description": "The change must not be applied now (superseded, discarded, failed, conflicted…); error.details.status names its state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "gone",
                    "message": "This change is superseded; do not apply it.",
                    "details": {
                      "status": "superseded"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/people/changes/{id}/confirmation": {
      "post": {
        "tags": [
          "People Changes"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Report what happened to a change",
        "description": "The receiver's answer for a change it fetched (its `confirm_url`).\n\n* `applied` — the change is in the destination system. The change is\n  marked confirmed; `external_id` (up to 255 characters) records the\n  destination's own id for it.\n* `rejected` — the destination refused it. Final: the change is marked\n  failed with `reason` (up to 200 characters) in the outbound log, and\n  an administrator can retry it from there.\n\nRequires an administrator's token with `write:people_changes`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The change id from the webhook's `change_id`.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "applied",
                      "rejected"
                    ]
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "The destination's id for the applied change (applied only)."
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Why the destination refused it (rejected only)."
                  }
                }
              },
              "example": {
                "status": "applied",
                "external_id": "ION-77"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change's state after the answer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "change": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "description": "confirmed after `applied`, failed after `rejected`"
                        },
                        "confirmed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    }
                  }
                },
                "example": {
                  "change": {
                    "id": 812,
                    "status": "confirmed",
                    "confirmed_at": "2026-09-24T15:04:40Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The token lacks write:people_changes, or its user is not an administrator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "description": "The change must not be applied now (superseded, discarded, failed, conflicted…); error.details.status names its state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "status is neither \"applied\" nor \"rejected\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_status",
                    "message": "status must be \"applied\" or \"rejected\"."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/ai_front_office/booking_requests/{id}/decline": {
      "post": {
        "tags": [
          "AI Front Office"
        ],
        "summary": "Decline a booking request",
        "description": "Declines a booking request that is waiting for review\n(`pending_review`) or approved by a dispatcher (`dispatcher_approved`):\nthe request becomes `declined`, a lost outcome is recorded with the\nreason, and a text telling the customer is queued — except for the\n`duplicate_request`, `customer_declined` and `cancelled_by_customer`\nreasons. The text is sent only when the business's texting setting, the\ncustomer's phone number and their opt-out status allow it.\n\n**Required Scopes:** `destructive:ai_front_office`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:ai_front_office` scope; `write:ai_front_office` (which\napprove asks) is not enough. A token without it is refused with 403\n`insufficient_permissions` (\"Required scope:\ndestructive:ai_front_office\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Booking request ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why the booking was lost. Any value outside the list is recorded as `other`; the response's `lost_reason` echoes the value recorded.",
                    "enum": [
                      "out_of_service_area",
                      "no_availability",
                      "customer_declined",
                      "duplicate_request",
                      "budget",
                      "cancelled_by_customer",
                      "other"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The booking request was declined.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "booking",
                    "lost_reason"
                  ],
                  "properties": {
                    "booking": {
                      "$ref": "#/components/schemas/AiFrontOfficeBookingRequest"
                    },
                    "lost_reason": {
                      "type": "string",
                      "example": "no_availability"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:ai_front_office\") or `forbidden` (the app is not available to the caller, or the caller is not a dispatcher).",
            "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` — \"Booking request not found\" 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": "`invalid_state` — the request is not waiting for review or approved (already declined, expired, converted, or captured in shadow mode); nothing changed. `decline_failed` — the decline could not be saved.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/analytics/sentiment/alerts/{id}/dismiss": {
      "post": {
        "tags": [
          "Analytics"
        ],
        "summary": "Dismiss a sentiment alert",
        "description": "Marks one sentiment alert dismissed, recording who dismissed it, when,\nand the optional reason. Admins, Communications app admins and people in\na leadership position may dismiss; a manager can read alerts but not dismiss them.\n\n**Required Scopes:** `destructive:analytics`\n\nDelete-level (owner decisions 2026-10-04, #6659 / #6721): requires the\n`destructive:analytics` scope; `read:analytics` is not enough. A token\nwithout it is refused with 403 `insufficient_permissions` (\"Required\nscope: destructive:analytics\") before the alert is looked up.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Sentiment alert ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why the alert was dismissed; stored as `dismissed_reason`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The alert was dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "alert"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "alert": {
                      "$ref": "#/components/schemas/SentimentAlert"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` in the standard error envelope (the token lacks `destructive:analytics`), or `{ \"ok\": false, \"error\": \"forbidden\" }` (no analytics access, or not allowed to dismiss alerts).",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/AnalyticsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`{ \"ok\": false, \"error\": \"not_found\" }` — no such alert in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/bulk_operations": {
      "post": {
        "tags": [
          "Bulk Operations"
        ],
        "summary": "Queue a bulk operation",
        "description": "Queues one bulk create, update or delete of `items` of one data type and\nanswers 201 with the queued operation; the work runs in the background.\nPoll `status_url` (GET /bulk_operations/status/{job_id}) for progress.\n\n**Token scopes:** by data type — `write:shifts` (shifts, assignments,\ndemand_signals, forecasts) or `write:availabilities` (availabilities,\npreferences). `destructive:scheduling` is asked for a final act:\ninstead of the write scope for a `delete` of shifts or assignments, and\non top of it for an assignment set to `cancelled` or `declined` and for\nan overwriting demand-signal or forecast import that sets `active: false`\n(owner decision 2026-10-04, #6721).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bulk_operation",
                  "items"
                ],
                "properties": {
                  "bulk_operation": {
                    "type": "object",
                    "required": [
                      "data_type"
                    ],
                    "properties": {
                      "data_type": {
                        "type": "string",
                        "enum": [
                          "shifts",
                          "assignments",
                          "availabilities",
                          "preferences",
                          "demand_signals",
                          "forecasts"
                        ]
                      },
                      "operation_type": {
                        "type": "string",
                        "enum": [
                          "create",
                          "update",
                          "delete"
                        ],
                        "default": "create"
                      }
                    }
                  },
                  "items": {
                    "$ref": "#/components/schemas/BulkOperationItems"
                  },
                  "options": {
                    "$ref": "#/components/schemas/BulkOperationOptions"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/BulkOperationQueued"
          },
          "400": {
            "description": "`parameter_missing` — the body has no `bulk_operation` 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": {
            "$ref": "#/components/responses/BulkOperationForbidden"
          },
          "422": {
            "$ref": "#/components/responses/BulkOperationRefused"
          }
        }
      }
    },
    "/bulk_operations/shifts": {
      "post": {
        "tags": [
          "Bulk Operations"
        ],
        "summary": "Queue a bulk shift operation",
        "description": "Queues a bulk create, update or delete of shifts. Each item is a shift\n(`id` for an update or a delete); an item's `assignments` are written\nwith it.\n\n**Token scopes:** `write:shifts`, or `destructive:scheduling` instead for\n`operation_type: delete`. An update — or a create with\n`options.conflict_resolution: overwrite` — whose items set an\nassignment's status to `cancelled` or `declined` also needs\n`destructive:scheduling` (owner decision 2026-10-04, #6721).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkOperationTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/BulkOperationQueued"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/BulkOperationForbidden"
          },
          "422": {
            "$ref": "#/components/responses/BulkOperationRefused"
          }
        }
      }
    },
    "/bulk_operations/assignments": {
      "post": {
        "tags": [
          "Bulk Operations"
        ],
        "summary": "Queue a bulk shift-assignment operation",
        "description": "Queues a bulk create, update or delete of shift assignments. A create\nitem names `shift_id` and `user_id` (an active member of the business);\nan update or a delete names the assignment's `id`.\n\n**Token scopes:** `write:shifts`, or `destructive:scheduling` instead for\n`operation_type: delete`. An update — or a create with\n`options.conflict_resolution: overwrite` — that sets an assignment's\n`status` to `cancelled` or `declined` also needs `destructive:scheduling`\n(owner decision 2026-10-04, #6721).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkOperationTypedRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "$ref": "#/components/responses/BulkOperationQueued"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/BulkOperationForbidden"
          },
          "422": {
            "$ref": "#/components/responses/BulkOperationRefused"
          }
        }
      }
    },
    "/bulk_operations/status/{job_id}": {
      "get": {
        "tags": [
          "Bulk Operations"
        ],
        "summary": "Get a bulk operation's progress",
        "description": "The progress of one queued operation — the caller's own, or any in the\nbusiness for an administrator (and, for a Shifts & Scheduling data type,\na Shifts & Scheduling app admin). Asks no scope beyond a valid token; an\nadmin or manager of the business only.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "The `job_id` the queueing answer returned.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The operation and its progress.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "bulk_operation",
                    "progress"
                  ],
                  "properties": {
                    "bulk_operation": {
                      "$ref": "#/components/schemas/BulkOperationSummary"
                    },
                    "progress": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "processed": {
                          "type": "integer"
                        },
                        "successful": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "percentage": {
                          "type": "number"
                        },
                        "duration": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/BulkOperationForbidden"
          },
          "404": {
            "description": "`{ \"error\": \"Bulk operation not found\" }` — none the caller may see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkOperationRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/calendar/events/{id}": {
      "delete": {
        "tags": [
          "Calendar"
        ],
        "summary": "Delete an event",
        "description": "Deletes an event the caller hosts (or hosts as a delegate), through the\nsame deletion the web app uses. When the event's online meeting could not\nbe cancelled at its provider, the event is still deleted and the answer\nis 200 with `meeting_warning` — the join link may still work.\n\nAn event booked through Appointment Scheduler cannot be deleted here:\ncancel the booking in Appointment Scheduler so the invitee is told (422\n`appointment_backed`; `details.manage_url` links to the booking when the\ncaller can open that app, otherwise null).\n\n**Required Scopes:** `destructive:calendar_sync`\n\nDelete-level (owner decision 2026-10-04, #6659): requires the\n`destructive:calendar_sync` scope; `write:calendar_sync` is not enough. A\ntoken without it is refused with 403 `insufficient_permissions`\n(\"Required scope: destructive:calendar_sync\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Calendar event ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted, but the online meeting's join link could not be cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "meeting_warning"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    },
                    "meeting_warning": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:calendar_sync\"), `app_disabled` (the Calendar app is not enabled for the business) or `forbidden` (the app is not open to the caller, or \"Only the host or their delegate may modify this event.\").",
            "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` — \"Event not found.\" (none in this business, or not visible to the caller).",
            "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": "`appointment_backed` (booked through Appointment Scheduler — see above) or `destroy_failed` (the event could not be deleted; `details` lists the reasons).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings": {
      "get": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Browse listings",
        "description": "The live listings Browse shows: active and not past their expiry date,\nnewest first unless `sort` says otherwise.\n\n**Required Scopes:** `read:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category_id",
            "in": "query",
            "required": false,
            "description": "Only listings in this category (an id from GET /classified_listings/categories). `category_id[]=` takes several.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "listing_type",
            "in": "query",
            "required": false,
            "description": "Only listings of this type; `listing_type[]=` takes several. An unknown value is refused with 400 `invalid_parameter`.",
            "schema": {
              "type": "string",
              "enum": [
                "for_sale",
                "free_giveaway",
                "wanted_request",
                "services_offered"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Words to find in the title, description or tags. Every word must match, in any order.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "`newest` (the default), `oldest`, `popular` (most viewed) or `favorites` (most favorited). `popular` and `favorites` answer newest first while the app's view counter or Favorites is off.",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "oldest",
                "popular",
                "favorites"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Listings per page, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "listings",
                    "meta"
                  ],
                  "properties": {
                    "listings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClassifiedListing"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ClassifiedPageMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` in the standard error envelope — a nested `search`, `listing_type` or `category_id`, or an unknown `listing_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"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Post a listing",
        "description": "Posts a listing as the caller, under the same rules as the page: it\nstarts as `pending_approval` when the app requires approval or its text\nmatches an auto-moderation keyword (the moderators are told), otherwise\n`active`. A free giveaway or wanted request is stored with no price.\nPhotos are uploaded as files (`classified_listing[photos][]`) in a\nmultipart request, within the app's photo count, size and file-type\nsettings.\n\n**Required Scopes:** `write:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "classified_listing"
                ],
                "properties": {
                  "classified_listing": {
                    "$ref": "#/components/schemas/ClassifiedListingInput"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "classified_listing"
                ],
                "properties": {
                  "classified_listing": {
                    "$ref": "#/components/schemas/ClassifiedListingInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Posted; `listing` is the new listing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "listing"
                  ],
                  "properties": {
                    "listing": {
                      "$ref": "#/components/schemas/ClassifiedListingDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`classified_listing` is missing (`parameter_missing`) or is not an object (`invalid_parameter`), in the standard error envelope.",
            "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": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Refused. `errors` lists why (a field the app requires, a category it does not allow, the posting limit, `tags` sent as a list); a photo of a type or size the app refuses answers `invalid_photos` in the standard error envelope instead.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ClassifiedErrors"
                    },
                    {
                      "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"
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/my_listings": {
      "get": {
        "tags": [
          "Classifieds"
        ],
        "summary": "List my listings",
        "description": "The caller's own listings in every state, newest first — live, awaiting\napproval, fulfilled, expired and removed. `status_text` says which kind\nof removal a `deleted` listing is (their own delete, a moderator's\ntakedown, or a rejection with the moderator's reason).\n\n**Required Scopes:** `read:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Listings per page, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the caller's listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "listings",
                    "meta"
                  ],
                  "properties": {
                    "listings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClassifiedMyListing"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ClassifiedPageMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` in the standard error envelope — a nested `search`, `listing_type` or `category_id` in the query.",
            "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": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/my_favorites": {
      "get": {
        "tags": [
          "Classifieds"
        ],
        "summary": "List my favorites",
        "description": "The listings the caller favorited that they can still see, most\nrecently favorited first. A removed listing is left out, and so is one\nawaiting approval unless it is the caller's or the caller moderates\nClassifieds.\n\n**Required Scopes:** `read:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Favorites per page, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of favorited listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "favorites",
                    "meta"
                  ],
                  "properties": {
                    "favorites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClassifiedListing"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ClassifiedPageMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_parameter` in the standard error envelope — a nested `search`, `listing_type` or `category_id` in the query.",
            "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": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`{ \"error\": \"Favorites are not enabled.\" }` — the app's Favorites setting is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/categories": {
      "get": {
        "tags": [
          "Classifieds"
        ],
        "summary": "List categories",
        "description": "The app's active categories, in the order the page lists them — the\nids `category_id` filters by and a new listing is posted under.\n`postable` is false for a category the app's prohibited categories\nsetting refuses for new listings; it stays listed so older listings in\nit can still be filtered to.\n\n**Required Scopes:** `read:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The categories.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "categories"
                  ],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClassifiedCategory"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/{id}": {
      "get": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Get a listing",
        "description": "One listing with its description, contact details and what the caller\nmay do with it (`can_edit`, `can_delete`). A listing awaiting approval\nis found only by its seller and by a business admin or Classifieds app\nadmin. Opening someone else's listing counts a view when the app's view\ncounter is on.\n\n**Required Scopes:** `read:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Listing ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The listing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "listing"
                  ],
                  "properties": {
                    "listing": {
                      "$ref": "#/components/schemas/ClassifiedListingDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }` — none in this business, removed, or awaiting approval and not the caller's to see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Edit a listing",
        "description": "Changes a listing that is live or awaiting approval; send only the\nfields that change (PUT is accepted too). Its seller may edit it, and a\nbusiness admin or Classifieds app admin may edit anyone's. New text\ngoes back for approval when the app requires approval or\nmatches an auto-moderation keyword; a lower price tells the people who\nfavorited it. Photos sent as files (`classified_listing[photos][]`) are\nadded to the ones it has; when they would pass the app's photo limit\nthe edit is still saved and `warnings` says why the photos were not.\n\n**Required Scopes:** `write:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Listing ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "classified_listing"
                ],
                "properties": {
                  "classified_listing": {
                    "$ref": "#/components/schemas/ClassifiedListingInput"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "classified_listing"
                ],
                "properties": {
                  "classified_listing": {
                    "$ref": "#/components/schemas/ClassifiedListingInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; `listing` is the listing as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "listing"
                  ],
                  "properties": {
                    "listing": {
                      "$ref": "#/components/schemas/ClassifiedListingDetail"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Present when the edit saved but the new photos were refused.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`classified_listing` is missing (`parameter_missing`) or is not an object (`invalid_parameter`), in the standard error envelope.",
            "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": "`{ \"error\": \"You cannot edit this listing.\" }` (not the caller's to edit, or fulfilled), `{ \"error\": \"This listing has expired. Renew it to edit it again.\" }` (an expired listing the caller may renew: `PATCH /classified_listings/{id}/extend_expiry` first), a scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          },
          "422": {
            "description": "Refused. `errors` lists why; a photo of a type or size the app refuses answers `invalid_photos` in the standard error envelope instead, before anything is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ClassifiedErrors"
                    },
                    {
                      "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": [
          "Classifieds"
        ],
        "summary": "Remove a listing",
        "description": "Removes a listing: its seller may remove their own, and a business admin\nor Classifieds app admin may remove anyone's. Such a removal is recorded\nas a moderator's, the seller is notified, and the listing's open reports\nare resolved. Removing\nsomeone else's listing with a token also needs `write:classified` on the\ntoken.\n\n**Required Scopes:** `destructive:classified`\n\nDelete-level (owner decision 2026-10-04, #6659): requires the\n`destructive:classified` scope; `write:classified` alone is not enough.\nA token without it is refused with 403 `insufficient_permissions`\n(\"Required scope: destructive:classified\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Listing ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The listing was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Listing deleted."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` in the standard error envelope (\"Required scope: destructive:classified\"), or `{ \"error\": \"…\" }` — the app is not enabled or not open to the caller, or the caller may not remove this listing.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }` — none in this business, or already removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          },
          "422": {
            "description": "The removal could not be saved; `errors` lists why.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/{id}/toggle_favorite": {
      "patch": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Favorite or unfavorite a listing",
        "description": "Favorites the listing, or unfavorites it when the caller already has.\nA seller cannot favorite their own listing.\n\n**Required Scopes:** `write:classified`\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Listing ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's favorite as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "favorited",
                    "favorites_count"
                  ],
                  "properties": {
                    "favorited": {
                      "type": "boolean",
                      "description": "True when the listing is now among the caller's favorites."
                    },
                    "favorites_count": {
                      "type": "integer",
                      "description": "How many people have favorited the listing."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A scope refusal, or the app is not enabled or not open to the caller.",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/ClassifiedsRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }` — none in this business, removed, or awaiting approval and not the caller's to see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          },
          "422": {
            "description": "`{ \"error\": \"Favorites are not enabled.\" }` (the app's Favorites setting is off) or `{ \"error\": \"You can't favorite your own listing.\" }`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifiedsRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/classified_listings/{id}/fulfill": {
      "patch": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Mark a listing fulfilled",
        "description": "Marks a live listing fulfilled (the page's **Mark as Fulfilled**): it\ncomes off Browse and cannot be reopened. People who saved it are told;\nwhen a business admin or Classifieds app admin closes someone else's\nlisting, its owner is told too. Closing someone else's listing with a\ntoken also needs `write:classified` on the token.\n\n**Required Scopes:** `destructive:classified`\n\nDelete-level (owner decisions 2026-10-04, #6659 and #6721): it cannot\nbe undone, so it asks `destructive:classified` — the scope MCP and Ask\nAI ask to fulfil a listing — and `write:classified` alone is not\nenough. A token without it is refused with 403\n`insufficient_permissions` (\"Required scope: destructive:classified\").\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fulfillment_notes",
            "in": "query",
            "required": false,
            "description": "An optional public note shown on the listing (\"Sold to a coworker\").",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Marked fulfilled; `listing` is the updated listing."
          },
          "403": {
            "description": "`{ \"error\": \"You cannot mark this listing as fulfilled.\" }`, or a scope refusal (\"Required scope: destructive:classified\")."
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }`"
          },
          "422": {
            "description": "The change could not be saved; `errors` lists why."
          }
        }
      }
    },
    "/classified_listings/{id}/extend_expiry": {
      "patch": {
        "tags": [
          "Classifieds"
        ],
        "summary": "Renew a listing",
        "description": "Renews a listing (the page's **Renew**): its expiry is set again,\ncounted from today, by the app's Auto-expire Listings setting. Allowed\nfor an expired listing or one within 7 days of expiring (a quarter of\nthe Auto-expire window when that is under 25 days). Renewing an\nexpired listing counts against the posting limit, and is refused when\nthe seller already has that many listings live or awaiting approval,\nor has left the organization.\n\n**Required Scopes:** `write:classified`\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Renewed; `expires_at` is the new expiry."
          },
          "403": {
            "description": "`{ \"error\": \"<why this listing can't be renewed>\" }`, or a scope refusal."
          },
          "404": {
            "description": "`{ \"error\": \"Listing not found.\" }`"
          },
          "422": {
            "description": "Refused while saving (for example the posting limit); `errors` lists why."
          }
        }
      }
    },
    "/mango_signal/score": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Company engagement score",
        "description": "The newest company-wide engagement score. Every value is null when no\nscore has been computed yet, and when the business switched the score\noff (`engagement_score_enabled: false`). Any member who can open Mango\nSignal may read it.\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The score.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "engagement_score_enabled": {
                      "type": "boolean"
                    },
                    "score": {
                      "type": "number",
                      "nullable": true
                    },
                    "stale": {
                      "type": "boolean",
                      "description": "The score's window ended before the current window began — it is the last score, not a current one."
                    },
                    "computed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "window_start": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "window_end": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "signal_count": {
                      "type": "integer",
                      "nullable": true
                    },
                    "breakdown": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": {
                        "type": "number"
                      },
                      "description": "Average score per signal type."
                    },
                    "is_sample_data": {
                      "type": "boolean",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          }
        }
      }
    },
    "/mango_signal/themes": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Top feedback themes",
        "description": "The most frequent themes in the window's signals. A theme backed by\nfewer people than the business's anonymity floor is left out. Any\nmember who can open Mango Signal may read it.\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MangoSignalAggregateWindowDays"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Themes to return, 1–200 (default 10).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The themes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer",
                      "description": "The window applied."
                    },
                    "sample_data_included": {
                      "type": "integer",
                      "description": "How many sample-data signals the window holds."
                    },
                    "themes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "polarity": {
                            "type": "string",
                            "nullable": true
                          },
                          "count": {
                            "type": "integer"
                          },
                          "avg_score": {
                            "type": "number",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          }
        }
      }
    },
    "/mango_signal/risk_segments": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Groups at risk",
        "description": "Locations, teams and segments whose engagement score is below the\nthreshold, worst first. A group smaller than the business's anonymity\nfloor (`min_group_size`) is never listed. Managers and admins only\n(403 `manager_access_required`).\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MangoSignalAggregateWindowDays"
          },
          {
            "name": "threshold",
            "in": "query",
            "required": false,
            "description": "Score below which a group is at risk, 0–1 (clamped). Defaults to the business's configured threshold, as does a value that is not a number.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Segments to return, 1–200 (default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The segments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "threshold": {
                      "type": "number"
                    },
                    "min_group_size": {
                      "type": "integer"
                    },
                    "window_days": {
                      "type": "integer"
                    },
                    "sample_data_included": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "More segments matched than `limit` returned."
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scope_type": {
                            "type": "string",
                            "enum": [
                              "location",
                              "team",
                              "segment"
                            ]
                          },
                          "scope_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "segment_key": {
                            "type": "string",
                            "nullable": true
                          },
                          "score": {
                            "type": "number"
                          },
                          "signal_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          }
        }
      }
    },
    "/mango_signal/signals": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "The signals feed",
        "description": "Individual signals, newest first. Each row can name the person it is\nabout (`user_id`), so this feed is for the people the business lets\nread individual signals — by default Mango Signal and business admins\n(403 `admin_access_required` otherwise). Free-text payloads are never\nreturned, and every read is recorded in the app's access log.\n\nPage through the feed with `offset` and `limit`; `total` is the size\nof the filtered set, `count` the rows in this response.\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MangoSignalRowWindowDays"
          },
          {
            "name": "source_app",
            "in": "query",
            "required": false,
            "description": "Only signals from this source app (repeat as `source_app[]` for several). A value that is not a string or an array of strings is refused (422 `invalid_source_app`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "signal_type",
            "in": "query",
            "required": false,
            "description": "Only signals of this type (repeat as `signal_type[]` for several; 422 `invalid_signal_type` as above).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "segment_key",
            "in": "query",
            "required": false,
            "description": "Only signals filed under this segment — its key, or its name as the dashboard shows it (repeat as `segment_key[]` for several, at most 50; 422 `invalid_segment_key` as above or above 50 values).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip before this page (default 0).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The signals.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The window applied; null when none was asked for."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Rows in this response."
                    },
                    "total": {
                      "type": "integer",
                      "description": "Signals matching the filters",
                      "before `offset` and `limit`.": null
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "segment_key": {
                      "nullable": true,
                      "description": "The segment filter applied (a key, or an array of keys).",
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      ]
                    },
                    "signals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "source_app": {
                            "type": "string"
                          },
                          "signal_type": {
                            "type": "string"
                          },
                          "score": {
                            "type": "number",
                            "nullable": true
                          },
                          "occurred_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "dated_only": {
                            "type": "boolean",
                            "description": "The signal records a day only; `occurred_at` is a placeholder time."
                          },
                          "occurred_on": {
                            "type": "string",
                            "format": "date",
                            "description": "The day",
                            "on the business's calendar.": null
                          },
                          "user_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "team_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "location_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "segment_key": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_sample_data": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          },
          "422": {
            "description": "A filter was sent in a shape that cannot be applied (`invalid_source_app`, `invalid_signal_type`, `invalid_segment_key`); nothing was filtered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/mango_signal/actions": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "List actions",
        "description": "Actions, soonest due first. A manager or admin sees every action; any\nother member sees the actions they own or are assigned.\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MangoSignalRowWindowDays"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One status, or `open_or_in_progress` for both live statuses. Any other value is refused (422 `invalid_status`).",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "in_progress",
                "complete",
                "dismissed",
                "open_or_in_progress"
              ]
            }
          },
          {
            "name": "assigned_to_me",
            "in": "query",
            "required": false,
            "description": "Only actions assigned to the caller.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return, 1–200 (default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The actions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Actions created in this many days; null when none was asked for."
                    },
                    "count": {
                      "type": "integer"
                    },
                    "actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MangoSignalAction"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          },
          "422": {
            "description": "`invalid_status` — an unknown `status` filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalRefusal"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Raise an action",
        "description": "Creates an action owned by the caller. Managers and admins only (403\n`manager_access_required`).\n\nA new action starts `open` or `in_progress`; `complete` or `dismissed`\nis refused (422 `invalid_status`) — create it, then complete or dismiss\nit with PATCH, so its \"you said, we did\" update and its impact\nmeasurement run.\n\nThe fields may be sent at the top level of the body or wrapped in\n`mango_signal_action` or `action`. An `Idempotency-Key` header makes a\nretry return the first success instead of writing again.\n\n**Token scopes:** `write:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A key you choose per write; a retry with the same key replays the stored success.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "in_progress"
                    ],
                    "default": "open"
                  },
                  "assignee_id": {
                    "type": "integer",
                    "description": "Defaults to the caller. Must be an active member of the business (422 `assignee_not_found`)."
                  },
                  "due_at": {
                    "type": "string",
                    "description": "The day it is due (`YYYY-MM-DD`); stored as that calendar day."
                  },
                  "scope_type": {
                    "type": "string",
                    "enum": [
                      "company",
                      "location",
                      "segment"
                    ]
                  },
                  "scope_id": {
                    "type": "integer",
                    "description": "The location, for a `location` scope. An id outside the caller's business is dropped and reported in `warnings`."
                  },
                  "segment_key": {
                    "type": "string",
                    "description": "The segment",
                    "for a `segment` scope.": null
                  },
                  "ai_recommended": {
                    "type": "boolean"
                  },
                  "signal_ids": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "type": "integer"
                    },
                    "description": "The signals this action answers. More than 1,000 is refused with 422 `too_many_signal_ids`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The action was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalAction"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          },
          "422": {
            "description": "`invalid_status` (a new action cannot start complete or dismissed), `assignee_not_found`, `too_many_signal_ids`, or `{ \"errors\": [...] }` when the action fails validation.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/MangoSignalRefusal"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_signal/actions/{id}": {
      "get": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Get an action",
        "description": "One action, with its description, completion notes and impact. A\nmanager or admin may read any action; any other member only one they\nown or are assigned (403 `manager_access_required`, with a message\nsaying who can open it).\n\n**Token scopes:** `read:mango_signal`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Action ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalAction"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/MangoSignalForbidden"
          },
          "404": {
            "description": "`not_found` — no such action in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalRefusal"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Mango Signal"
        ],
        "summary": "Update an action",
        "description": "Updates an action. Its owner, a business admin and a Mango Signal app\nadmin may change any field; its assignee may only move `status` among\n`open`, `in_progress` and `complete` and write `completion_notes` (other\nfields are ignored, and any other status is refused with 422\n`status_not_permitted`). Dismissing an action is therefore its owner's\nor an admin's decision.\n\nA completed or dismissed action is closed: this endpoint refuses any\nwrite to it (422 `action_not_actionable`, with a message saying who can\nreopen it from the web app).\n\nCompleting an action (`status` moving to `complete`) runs the \"you\nsaid, we did\" follow-up; `close_loop` in the response reports whether it\nwent out (`fired`, `fired_without_impact_baseline` or `not_fired`).\n\nThe fields may be sent at the top level of the body or wrapped in\n`mango_signal_action` or `action`. An `Idempotency-Key` header makes a\nretry return the first success instead of writing again.\n\n**Token scopes:** `write:mango_signal`, and also\n`destructive:mango_signal` when `status` is `dismissed` — dismissing an\naction is a final act (owner decision 2026-10-04, #6721). Any other\nupdate needs only the write scope.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Action ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A key you choose per write; a retry with the same key replays the stored success.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "in_progress",
                      "complete",
                      "dismissed"
                    ],
                    "description": "`dismissed` also needs `destructive:mango_signal`, and only the owner or an admin may set it."
                  },
                  "completion_notes": {
                    "type": "string"
                  },
                  "announce_company_wide": {
                    "type": "boolean",
                    "description": "Read only when this request completes the action: also post the update to everyone, for an action whose scope has no people list."
                  },
                  "title": {
                    "type": "string",
                    "description": "Owner or admin only."
                  },
                  "description": {
                    "type": "string",
                    "description": "Owner or admin only."
                  },
                  "assignee_id": {
                    "type": "integer",
                    "description": "Owner or admin only. Must be an active member of the business (422 `assignee_not_found`)."
                  },
                  "due_at": {
                    "type": "string",
                    "description": "Owner or admin only. The day it is due (`YYYY-MM-DD`); stored as that calendar day."
                  },
                  "scope_type": {
                    "type": "string",
                    "description": "Owner or admin only."
                  },
                  "scope_id": {
                    "type": "integer",
                    "description": "Owner or admin only. An id outside the caller's business is dropped and reported in `warnings`."
                  },
                  "segment_key": {
                    "type": "string",
                    "description": "Owner or admin only."
                  },
                  "ai_recommended": {
                    "type": "boolean",
                    "description": "Owner or admin only."
                  },
                  "signal_ids": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "type": "integer"
                    },
                    "description": "Owner or admin only: the signals this action answers. More than 1,000 is refused with 422 `too_many_signal_ids`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The action was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The action. `scope_type`, `segment_key`, `scope_id` (null with a `scope_label` for an action about one individual) and `signal_ids` appear only for a manager-tier caller.",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "title": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "assignee_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "assignee_name": {
                      "type": "string",
                      "nullable": true
                    },
                    "assignee_active": {
                      "type": "boolean"
                    },
                    "owner_id": {
                      "type": "integer"
                    },
                    "owner_name": {
                      "type": "string",
                      "nullable": true
                    },
                    "owner_active": {
                      "type": "boolean"
                    },
                    "due_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "due_date": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    },
                    "completed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "ai_recommended": {
                      "type": "boolean"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "is_sample_data": {
                      "type": "boolean"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "completion_notes": {
                      "type": "string",
                      "nullable": true
                    },
                    "source_survey": {
                      "type": "object",
                      "nullable": true
                    },
                    "impact": {
                      "type": "object",
                      "nullable": true
                    },
                    "scope_type": {
                      "type": "string"
                    },
                    "segment_key": {
                      "type": "string",
                      "nullable": true
                    },
                    "scope_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "scope_label": {
                      "type": "string"
                    },
                    "signal_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "close_loop": {
                      "type": "string",
                      "enum": [
                        "fired",
                        "fired_without_impact_baseline",
                        "not_fired"
                      ],
                      "description": "Present only when this request completed the action."
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Present only when a submitted `scope_id` was dropped."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (`code`; `error` names the missing scope — `destructive:mango_signal` for a dismissal), `mango_signal_disabled`, or `action_access_required` (not the owner, the assignee or an admin).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalRefusal"
                }
              }
            }
          },
          "404": {
            "description": "`{ \"error\": \"not_found\" }` — no such action in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSignalRefusal"
                }
              }
            }
          },
          "422": {
            "description": "`action_not_actionable` (the action is complete or dismissed), `status_not_permitted` (an assignee asked for a status they may not set), `assignee_not_found`, `too_many_signal_ids`, or `{ \"errors\": [...] }` when the action fails validation.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/MangoSignalRefusal"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/summary": {
      "get": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Get your Mango Spend summary",
        "description": "The caller's spend roll-up: their drafts, reports in flight, returned\nclaims and paid-this-year totals. An approver also gets what is waiting\nfor their decision, and a caller who may view all spend also gets the\ncompany's payout figures (see the response fields).\n\n**Required Scopes:** `read:mango_spend` (`read:own_mango_spend` also\npasses, but then the approver and company blocks are left out — only the\npersonal block is answered).\n\nNeeds Reimbursements switched on for the business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MangoSpendSummary"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports": {
      "get": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "List expense reports",
        "description": "Expense reports the caller may see, most recently updated first. That is\nthe caller's own reports plus those the app's visibility rule opens to\nthem (the filed reports of the people they manage; every filed report\nfor a Mango Spend admin); other people's drafts are never listed. A token\nholding only `read:own_mango_spend` sees only the caller's own reports.\n\n**Required Scopes:** `read:mango_spend` (`read:own_mango_spend` also\npasses).\n\nNeeds Reimbursements switched on for the business. Paginated: the\n`reports` key is unchanged and `meta` plus the `X-Total-Count`,\n`X-Total-Pages`, `X-Current-Page` and `X-Per-Page` headers describe the\nwhole filtered set.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status[]",
            "in": "query",
            "required": false,
            "description": "Only reports in these statuses. Repeat it (`status[]=draft&status[]=submitted`) to take several; one status may also be sent as a plain `status=draft`. Repeating the plain `status=` keeps only the last value. A value that is not a status matches nothing (an empty list), it does not widen the list.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "draft",
                  "submitted",
                  "approved",
                  "rejected",
                  "sent_back",
                  "paid",
                  "cancelled"
                ]
              }
            },
            "style": "form",
            "explode": true
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Anything that is not a positive number means the first page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Reports per page. Anything that is not a positive number means 25; more than 100 is cut to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of reports (the short form, without lines).",
            "headers": {
              "X-Total-Count": {
                "description": "Reports in the whole filtered set.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages in the whole filtered set.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page returned.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "Reports per page as applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reports",
                    "meta"
                  ],
                  "properties": {
                    "reports": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MangoSpendExpenseReport"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/MangoSpendPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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": [
          "Mango Spend"
        ],
        "summary": "Start an expense report",
        "description": "Creates a draft report owned by the caller. Add lines with\n`POST /mango_spend/expense_reports/{id}/lines` and file it with\n`POST /mango_spend/expense_reports/{id}/submit`.\n\n**Required Scopes:** `write:mango_spend` (`write:own_mango_spend` also\npasses — the report is the caller's own).\n\nNeeds Reimbursements switched on for the business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expense_report"
                ],
                "properties": {
                  "expense_report": {
                    "type": "object",
                    "required": [
                      "title"
                    ],
                    "properties": {
                      "title": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Required."
                      },
                      "purpose": {
                        "type": "string",
                        "nullable": true
                      },
                      "currency": {
                        "type": "string",
                        "description": "Three-letter currency code (USD, EUR, GBP, CAD or AUD). Used only when the business has multi-currency on; otherwise, and when omitted or not one of those five, the report is filed in the business's default currency — silently, with no error. Every line the report gets is stamped with the report's currency."
                      },
                      "report_period_start": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "description": "Not in the future."
                      },
                      "report_period_end": {
                        "type": "string",
                        "format": "date",
                        "nullable": true,
                        "description": "Not in the future, and not before `report_period_start`."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft report (detailed form).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — no `expense_report` object in the body (or it is 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": "`insufficient_permissions` (\"Required scope: write:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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_report` — the report failed validation (no title, a title over 200 characters, a future or backwards period); the message lists the problems.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports/{id}": {
      "get": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Get an expense report",
        "description": "One report in the detailed form: its purpose, lines (each with its\nreceipts) and policy violations. Opens the caller's own report, and any\nreport the caller may read as its approver or a Mango Spend admin; a\ntoken holding only `read:own_mango_spend` opens only the caller's own.\n\n**Required Scopes:** `read:mango_spend` (`read:own_mango_spend` also\npasses).\n\nWith Reimbursements switched off the caller's own reports (and drafts)\nanswer 403 `reimbursements_disabled`; a filed report that someone else\nowns and the caller may read still opens, so an approver can open the\nreport they are deciding.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report (detailed form).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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 report, or one the caller may not read.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports/{id}/submit": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Submit your expense report",
        "description": "Files one of the caller's own reports for approval. The report's policy\nrules are checked first; a blocking violation (a line with no amount, a\nmissing receipt the policy requires) refuses the submit and is listed in\n`error.details.violations`. On success the report moves to `submitted`,\nits approval chain starts and the approver is told.\n\n**Required Scopes:** `write:mango_spend` (`write:own_mango_spend` also\npasses).\n\nNeeds Reimbursements switched on for the business. Takes no request body.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID (one of the caller's own)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The submitted report (detailed form).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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 report of the caller's.",
            "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": "`policy_blocked` (\"Resolve blocking policy violations before submitting.\"; `error.details.violations` lists them as MangoSpendPolicyViolation objects) or `illegal_transition` (the report cannot move to submitted from its current status, or another rule refuses it — the message says which).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports/{id}/lines": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Add a line to your expense report",
        "description": "Adds one line (an expense, a mileage claim or a per-diem day) to one of\nthe caller's own reports while it is still open for editing (`draft` or\n`sent_back`). The line takes the report's currency; a `currency` in the\nbody is ignored. Mileage and per-diem lines are priced from the\nbusiness's rates — a rate cannot be supplied.\n\n**Required Scopes:** `write:mango_spend` (`write:own_mango_spend` also\npasses).\n\nNeeds Reimbursements switched on for the business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID (one of the caller's own)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "line"
                ],
                "properties": {
                  "line": {
                    "$ref": "#/components/schemas/MangoSpendLineInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new line.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "line"
                  ],
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/MangoSpendExpenseLine"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — no `line` object in the body (or it is 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": "`insufficient_permissions` (\"Required scope: write:mango_spend\"), `app_not_enabled`, `app_access_denied`, `reimbursements_disabled`, `mileage_disabled` (a mileage line while Mileage is switched off in Settings) or `per_diem_disabled` (a per-diem line while Per-diem rates are switched off).",
            "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 report of the caller's.",
            "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": "`report_not_editable` (\"This report is <status> and can no longer be changed.\") or `invalid_line` (the line failed validation — an expense with no amount, a mileage line with no miles, a per-diem line with no days or `location_code`, an unknown `line_type`, a future `spent_on` on an expense, an archived category; the message lists the problems).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports/{id}/receipts": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Attach a receipt to your expense report",
        "description": "Uploads a receipt (an image or PDF) to one of the caller's own reports\nwhile it is still open for editing. With `line_id` the receipt goes on\nthat line; without it a placeholder expense line (`needs_amount: true`,\n$0, today's date) is made for the receipt reader to fill — the report\ncannot be submitted until that line has an amount, so price it with\n`PATCH /mango_spend/expense_reports/{id}/lines/{line_id}` or remove it\nif the reading fails. The file's type, size and actual content are\nchecked; a file that is not a receipt is refused.\n\n**Required Scopes:** `write:mango_spend` (`write:own_mango_spend` also\npasses).\n\nNeeds Reimbursements switched on for the business.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID (one of the caller's own)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The receipt, as a multipart file part. A plain text value is refused (422 `invalid_file`)."
                  },
                  "line_id": {
                    "type": "integer",
                    "description": "Attach to this line of the report. Omitted: a placeholder line is made. A line that is not on this report answers 404."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The stored receipt and the line it is on.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "receipt",
                    "line"
                  ],
                  "properties": {
                    "receipt": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "line_id": {
                          "type": "integer"
                        },
                        "extraction_status": {
                          "type": "string",
                          "description": "Where the receipt reader is with the file, `pending` when just stored.",
                          "example": "pending"
                        }
                      }
                    },
                    "line": {
                      "$ref": "#/components/schemas/MangoSpendExpenseLine"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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 report of the caller's, or no such line on it.",
            "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": "`report_not_editable`; `missing_file` (\"A file upload is required.\"); `invalid_file` (`file` is not a multipart upload, or its type, size or content is not an acceptable receipt — the message says why); or `invalid_receipt` (the receipt could not be stored). Nothing is left behind on a failure — no placeholder line.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/approvals": {
      "get": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "List reports waiting for your decision",
        "description": "The approver's queue, oldest submission first: the submitted reports\nwaiting on the caller (as the submitter's manager or a named approver in\nan approval chain). A Mango Spend admin sees every submitted report in\nthe business except their own. Each row is the short report form plus\n`employee` — the submitter's name, else their email.\n\n**Required Scopes:** `read:mango_spend` — the wide grant; a\n`read:own_mango_spend` token is refused, because the queue holds other\npeople's reports.\n\nOnly people who approve reports have a queue: a Mango Spend admin,\nsomeone with direct reports, or anyone named in an approval chain.\nEveryone else gets 403 `not_a_manager`. Reachable with Reimbursements\nswitched off. Paginated like the report list.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number. Anything that is not a positive number means the first page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Reports per page. Anything that is not a positive number means 25; more than 100 is cut to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the queue.",
            "headers": {
              "X-Total-Count": {
                "description": "Reports in the whole queue.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Total-Pages": {
                "description": "Pages in the whole queue.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Current-Page": {
                "description": "The page returned.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Per-Page": {
                "description": "Reports per page as applied.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "reports",
                    "meta"
                  ],
                  "properties": {
                    "reports": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MangoSpendExpenseReport"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "employee": {
                                "type": "string",
                                "nullable": true,
                                "description": "The submitter's name, else their email."
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/MangoSpendPaginationMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: read:mango_spend\" or \"Required scope: read:mango_spend (read:own_mango_spend only covers your own reports).\"), `app_not_enabled`, `app_access_denied` or `not_a_manager` (the caller approves no one's reports).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/approvals/{id}/approve": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Approve an expense report",
        "description": "Approves the approval step the caller holds on a submitted expense\nreport (a Mango Spend app admin may decide any submitted report except\ntheir own). When the report has an approval chain with more steps, the\ndecision advances it to the next approver (`approval_chain_completed`\nfalse, the report stays `submitted`); the final approval ends the chain\nand the report becomes `approved`. The submitter, or the next approver,\nis told.\n\n**Token scopes:** `write:mango_spend` — deciding another employee's\nreport needs the wide write scope (`write:own_mango_spend` covers only\nyour own reports, and you cannot decide your own). Approving is not\ndelete-level, so `destructive:mango_spend` is not asked (rejecting is).\nA token missing it is refused with 403 `insufficient_permissions`.\n\nReachable with Reimbursements switched off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "An optional note stored with the decision. Trimmed and cut to 2,000 characters; a non-string value counts as none."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report",
                    "approval_chain_completed"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    },
                    "approval_chain_completed": {
                      "type": "boolean",
                      "description": "Whether the decision ended the report's approval chain."
                    },
                    "decided_in_place_of": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when a Mango Spend admin decided a report that was waiting on someone else (its manager or a chain step): who was skipped, in the sentence the review page shows before the decision. Null when the report was the caller's to decide."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:mango_spend\" or \"Required scope: write:mango_spend (write:own_mango_spend cannot decide another employee's report).\"), `app_not_enabled` or `app_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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the report is not waiting for the caller's decision (a report of the caller's own never is). When it was taken back to draft, or already decided by someone else, the message says so.",
            "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": "`stale_decision` — this approval step was already decided, or another approver decided (or the submitter took back) the report in the same moment. In the second case the message says who; in the first it says only that the step was already decided.",
            "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": "`illegal_transition` — it is waiting for a different approver, or it cannot move to approved from its current status.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/approvals/{id}/reject": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Reject an expense report",
        "description": "Rejects the approval step the caller holds on a submitted expense report\n(a Mango Spend app admin may decide any submitted report except their\nown). The submitter is told; the report ends `rejected`.\n\n**Token scopes:** `destructive:mango_spend` and `write:mango_spend` —\nrejecting is a final act (owner decision 2026-10-04, #6721), and\ndeciding another employee's report needs the wide write scope\n(`write:own_mango_spend` covers only your own reports). A token missing\neither is refused with 403 `insufficient_permissions`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "comment"
                ],
                "properties": {
                  "comment": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "The reason for the rejection, shown to the submitter. Required (422 `illegal_transition`, \"Reason for rejection is required.\"). Trimmed and cut to 2,000 characters; a non-string value counts as none."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The approval step was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report",
                    "approval_chain_completed"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    },
                    "approval_chain_completed": {
                      "type": "boolean",
                      "description": "Whether the decision ended the report's approval chain."
                    },
                    "decided_in_place_of": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when a Mango Spend admin decided a report that was waiting on someone else (its manager or a chain step): who was skipped, in the sentence the review page shows before the decision. Null when the report was the caller's to decide."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:mango_spend\", or \"Required scope: write:mango_spend (write:own_mango_spend cannot decide another employee's report).\"), `app_not_enabled` or `app_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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the report is not waiting for the caller's decision (a report of the caller's own never is). When it was taken back to draft, or already decided by someone else, the message says so.",
            "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": "`stale_decision` — this approval step was already decided, or another approver decided (or the submitter took back) the report in the same moment. In the second case the message says who; in the first it says only that the step was already decided.",
            "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": "`illegal_transition` — no `comment` was given, it is waiting for a different approver, or it cannot move to rejected from its current status.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/approvals/{id}/request_changes": {
      "post": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Send an expense report back for changes",
        "description": "Sends a submitted expense report the caller may decide back to its\nowner to fix and submit again — the desktop and phone \"Request\nchanges\". The report becomes `sent_back` (\"Changes Requested\"), ends\nits approval chain, and the owner is told. Unlike a rejection it is not\nfinal. A Mango Spend app admin may decide any submitted report except\ntheir own.\n\n**Token scopes:** `write:mango_spend` — deciding another employee's\nreport needs the wide write scope (`write:own_mango_spend` covers only\nyour own reports). Sending back is not delete-level, so\n`destructive:mango_spend` is not asked. A token missing it is refused\nwith 403 `insufficient_permissions`.\n\nReachable with Reimbursements switched off.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What the owner should change, shown to them on the report and in their notice. Optional. Trimmed and cut to 2,000 characters; a non-string value counts as none."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report was sent back.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "report",
                    "approval_chain_completed"
                  ],
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    },
                    "approval_chain_completed": {
                      "type": "boolean",
                      "description": "Whether the decision ended the report's approval chain (it always does)."
                    },
                    "decided_in_place_of": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when a Mango Spend admin decided a report that was waiting on someone else (its manager or a chain step): who was skipped, in the sentence the review page shows before the decision. Null when the report was the caller's to decide."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:mango_spend\" or \"Required scope: write:mango_spend (write:own_mango_spend cannot decide another employee's report).\"), `app_not_enabled` or `app_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"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — the report is not waiting for the caller's decision (a report of the caller's own never is). When it was taken back to draft, or already decided by someone else, the message says so.",
            "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": "`stale_decision` — this approval step was already decided, or another approver decided (or the submitter took back) the report in the same moment. In the second case the message says who; in the first it says only that the step was already decided.",
            "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": "`illegal_transition` — it is waiting for a different approver, or it cannot be sent back from its current status.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/mango_spend/expense_reports/{id}/lines/{line_id}": {
      "patch": {
        "tags": [
          "Mango Spend"
        ],
        "summary": "Change a line on your expense report",
        "description": "Updates one line on one of the caller's own reports while it is still\nopen for editing — the way to price the $0 placeholder line a receipt\nupload made. Send only the fields to change; the line keeps the currency\nit was filed in, and mileage and per-diem rates cannot be set. The\nreport's totals follow the change.\n\n**Required Scopes:** `write:mango_spend` (`write:own_mango_spend` also\npasses).\n\nNeeds Reimbursements switched on for the business. Changing a line into\na mileage or per-diem line is refused (403) while that module is\nswitched off; a line that already is one can still be edited.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID (one of the caller's own)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "line_id",
            "in": "path",
            "required": true,
            "description": "Expense line ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "line"
                ],
                "properties": {
                  "line": {
                    "$ref": "#/components/schemas/MangoSpendLineInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated line.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "line"
                  ],
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/MangoSpendExpenseLine"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`parameter_missing` — no `line` object in the body (or it is 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": "`insufficient_permissions` (\"Required scope: write:mango_spend\"), `app_not_enabled`, `app_access_denied`, `reimbursements_disabled`, `mileage_disabled` or `per_diem_disabled` (the line is being changed into a type that is switched off).",
            "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 report of the caller's, or no such line on it.",
            "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": "`report_not_editable` or `invalid_line` (the line failed validation; the message lists the problems).",
            "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": [
          "Mango Spend"
        ],
        "summary": "Remove a line from your expense report",
        "description": "Removes one line from one of the caller's own reports while the report is\nstill open for editing.\n\n**Required Scopes:** `destructive:mango_spend`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:mango_spend` scope; `write:mango_spend` is not enough. A\ntoken without it is refused with 403 `insufficient_permissions`\n(\"Required scope: destructive:mango_spend\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Expense report ID (one of the caller's own)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "line_id",
            "in": "path",
            "required": true,
            "description": "Expense line ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The line was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "line",
                    "report"
                  ],
                  "properties": {
                    "line": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "deleted": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    },
                    "report": {
                      "$ref": "#/components/schemas/MangoSpendExpenseReport"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:mango_spend\"), `app_not_enabled`, `app_access_denied` or `reimbursements_disabled`.",
            "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 report of the caller's, or no such line on it.",
            "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": "`report_not_editable` (the report is no longer open for editing) or `invalid_line` (the line could not be removed).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/password_manager/entries/{id}": {
      "delete": {
        "tags": [
          "Password Manager"
        ],
        "summary": "Delete a password entry",
        "description": "Deletes an entry for good, with an audit record of the deletion; when\nthat record cannot be written the entry is left in place and the answer\nis 500.\n\n**Required Scopes:** `destructive:password_manager`\n\nDelete-level (owner decision 2026-10-04, #6659): requires the\n`destructive:password_manager` scope; `write:passwords` is not enough. A\ntoken without it is refused with 403 `insufficient_permissions`\n(\"Required scope: destructive:password_manager\").\n\nDeleting or archiving an entry the caller does not own also needs\n`write:passwords` on the token (`write:own_passwords` reaches only the\ncaller's own entries): 403 \"You do not have permission to edit this\npassword\".\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Password entry ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The entry was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Password 'Payroll portal' deleted successfully"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` in the standard error envelope, or `{ \"success\": false, \"error\": \"…\" }` — the app is not enabled or not open to the caller, \"You do not have permission to delete this password\", or \"You do not have permission to edit this password\" (an entry the caller does not own, on a token without `write:passwords`).",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/PasswordManagerRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "\"Password entry not found\" — none in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasswordManagerRefusal"
                }
              }
            }
          },
          "500": {
            "description": "The deletion could not be recorded, so the entry was left in place. Try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasswordManagerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/password_manager/entries/{id}/archive": {
      "patch": {
        "tags": [
          "Password Manager"
        ],
        "summary": "Archive a password entry",
        "description": "Archives an entry the caller may edit. Archiving an entry that is\nalready archived succeeds and changes nothing. `PATCH\n/password_manager/entries/{id}/restore` (`write:passwords`) brings it\nback.\n\n**Required Scopes:** `destructive:password_manager`\n\nDelete-level (owner decision 2026-10-04, #6659): requires the\n`destructive:password_manager` scope; `write:passwords` is not enough. A\ntoken without it is refused with 403 `insufficient_permissions`\n(\"Required scope: destructive:password_manager\").\n\nDeleting or archiving an entry the caller does not own also needs\n`write:passwords` on the token (`write:own_passwords` reaches only the\ncaller's own entries): 403 \"You do not have permission to edit this\npassword\".\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Password entry ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The entry is archived.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "message",
                    "entry"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Password archived"
                    },
                    "entry": {
                      "$ref": "#/components/schemas/PasswordManagerEntrySummary"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` in the standard error envelope, or `{ \"success\": false, \"error\": \"…\" }` — the app is not enabled or not open to the caller, or \"You do not have permission to edit this password\".",
            "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"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/PasswordManagerRefusal"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "\"Password entry not found\" — none in this business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasswordManagerRefusal"
                }
              }
            }
          },
          "422": {
            "description": "The entry could not be archived; `errors` lists why (`error_code` `personal_cap` when a personal-entry limit stands in the way).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasswordManagerRefusal"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be recorded, so nothing was archived. Try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasswordManagerRefusal"
                }
              }
            }
          }
        }
      }
    },
    "/recorder/config": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "Recorder settings, preferences and capabilities",
        "description": "The desktop's tenant gate. Answers the workspace's Recorder settings a\nclient can act on, the caller's own defaults, and what this caller and\ntoken may do. A tenant without the app answers 403 `app_not_enabled`;\nthe desktop then turns off cloud actions only (capture stays local).\nThe same block is `apps.recorder` in `GET /desktop/bootstrap`.\n\n**Token scopes:** none — Recorder reads are not scope-gated. The\n`capabilities` flags do read the token: `upload`, `share` and\n`public_links` need `write:recorder`; `send_to_chat` also needs\n`write:chat`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The Recorder block.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recorder"
                  ],
                  "properties": {
                    "recorder": {
                      "$ref": "#/components/schemas/RecorderConfig"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` (Recorder is off for this workspace) or `app_not_accessible` (it is not open to the caller).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/pusher/auth": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Authorize the caller's Recorder realtime channel",
        "description": "Signs a subscription to the one channel the desktop listens on,\n`private-user-recorder-{userId}-business-{businessId}`, for the\ncaller's own user and business ids and nobody else's. The Pusher key\nand cluster are not served here — they come from `GET /chat/config`.\n\nThe answer is the raw Pusher auth payload, not wrapped in an envelope,\nso it can be handed straight to the Pusher client.\n\n**Token scopes:** `write:recorder` (it is a POST).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecorderPusherAuthRequest"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/RecorderPusherAuthRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pusher auth payload (returned direct, un-enveloped).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` — `channel_name` or `socket_id` is missing, or `socket_id` is malformed.",
            "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": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible` or `forbidden` (\"Unsupported channel.\" — not the caller's own Recorder channel).",
            "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": "`pusher_unconfigured` — realtime is not configured on this server.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/preferences": {
      "patch": {
        "tags": [
          "Recorder"
        ],
        "summary": "Update the caller's own Recorder defaults",
        "description": "Saves the caller's own defaults — the same values the web Preferences\npage edits and `GET /recorder/config` reads back. Keys the request\nleaves out are kept. While the workspace enforces its default\nvisibility (`visibility_enforced`), `default_visibility` is ignored.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send only the fields to change. Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "default_visibility": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "private",
                      "company",
                      "anyone",
                      ""
                    ],
                    "description": "The audience new captures get. Blank or null clears it (the workspace default applies), except that a stored choice the workspace no longer allows is kept. `anyone` needs public links turned on (422 `public_links_disabled`) and the right to share externally (403 `share_externally_not_permitted`)."
                  },
                  "default_transcribe": {
                    "type": "boolean",
                    "description": "Whether new captures are transcribed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The defaults as they now stand.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "preferences"
                  ],
                  "properties": {
                    "preferences": {
                      "$ref": "#/components/schemas/RecorderPreferences"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible` or `share_externally_not_permitted` (the caller may not publish on a public link, so `anyone` cannot be their default).",
            "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_visibility`, `public_links_disabled`, `<field>_required` (a field sent as a list or an object) or `validation_error` (with an `errors` list).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "List recordings",
        "description": "The caller's library, newest first by default.\n\n- `scope=all` (default): every live recording the caller can watch.\n- `scope=mine`: the caller's own live recordings (`mine=1` is the older spelling).\n- `scope=shared`: what the caller can watch beyond their own — company-visible and shared with them.\n- `scope=deleted`: the recycle bin — the caller's own, or the whole\n  workspace's for a Recorder admin. A row in it the caller cannot watch\n  comes in the recycle-bin shape (`RecorderRecycleBinRecording`).\n\n`sort=title` and `scope=deleted` page by offset; every other list pages\nby recording id. `cursor_type` says which kind `next_cursor` is, so a\nclient that changes the sort can reset its cursor. Pass `next_cursor`\nback as `cursor` for the next page; `total` counts the filtered set.\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Which recordings. An unknown value lists `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "mine",
                "shared",
                "deleted"
              ],
              "default": "all"
            }
          },
          {
            "name": "mine",
            "in": "query",
            "required": false,
            "description": "The older spelling of `scope=mine` (read only when `scope` is `all`).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Only this kind. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "video",
                "screenshot"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Title search. Trimmed and cut to 100 characters.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Order. An unknown value sorts `newest`.",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "title"
              ],
              "default": "newest"
            }
          },
          {
            "$ref": "#/components/parameters/RecorderCursor"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of recordings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recordings",
                    "scope",
                    "sort",
                    "total",
                    "cursor_type",
                    "next_cursor"
                  ],
                  "properties": {
                    "recordings": {
                      "type": "array",
                      "items": {
                        "anyOf": [
                          {
                            "$ref": "#/components/schemas/RecorderRecording"
                          },
                          {
                            "$ref": "#/components/schemas/RecorderRecycleBinRecording"
                          }
                        ]
                      }
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "all",
                        "mine",
                        "shared",
                        "deleted"
                      ]
                    },
                    "sort": {
                      "type": "string",
                      "enum": [
                        "newest",
                        "title"
                      ]
                    },
                    "total": {
                      "type": "integer",
                      "description": "Recordings matching the filters, across every page."
                    },
                    "cursor_type": {
                      "type": "string",
                      "enum": [
                        "id",
                        "offset"
                      ]
                    },
                    "next_cursor": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The `cursor` for the next page; null on the last page."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": "`q_required`, `kind_required`, `scope_required` or `sort_required` — that parameter was sent as a list or 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"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Create a recording",
        "description": "Creates the row a capture's bytes are then uploaded to (`media/init` +\n`media/complete`, or the multipart `media`). The new recording is\n`uploading` until its bytes arrive.\n\nThe same `client_capture_id` from the same person returns the existing\nrow (200, `existing: true`) instead of a twin — revived from the recycle\nbin if needed, and put back to `uploading` if it never got its bytes.\n\nFree text is cut, never refused: `title` is trimmed and cut to 150\ncharacters. A value outside its range (`captured_at`, `duration_ms`,\n`width`, `height`) is stored as empty rather than refused.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Every field is optional. Each except `metadata` must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "video",
                      "screenshot"
                    ],
                    "default": "video"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 150,
                    "description": "Trimmed and cut to 150 characters."
                  },
                  "client_capture_id": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "The client's own id for this capture; makes a retried create return the same row (422 `client_capture_id_too_long` over 255 characters)."
                  },
                  "captured_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it was captured. Unparseable, or outside the years 1970–9999, is stored as empty."
                  },
                  "duration_ms": {
                    "type": "integer",
                    "description": "Length in milliseconds, stored as whole seconds. Only 1 ms to 24 hours is kept."
                  },
                  "width": {
                    "type": "integer",
                    "description": "Pixels, 1–16,384; anything else is stored as empty."
                  },
                  "height": {
                    "type": "integer",
                    "description": "Pixels, 1–16,384; anything else is stored as empty."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "company",
                      "anyone"
                    ],
                    "description": "Used when the workspace allows it for this person and does not enforce its own default; otherwise the caller's default (or the workspace's) applies. Never refused."
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "desktop",
                      "web",
                      "mobile"
                    ],
                    "default": "desktop",
                    "description": "Any other value is stored as `desktop`."
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Free-form capture facts. At most 20 keys are kept, each cut to 64 characters; values must be a string (cut to 200 characters), a boolean or a number under 10^15 in size — anything else is dropped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The recording was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderCreatedRecording"
                }
              }
            }
          },
          "200": {
            "description": "This person already has a recording with that `client_capture_id`; it is returned instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderCreatedRecording"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled` or `app_not_accessible`.",
            "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": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "422": {
            "description": "`invalid_kind`, `client_capture_id_too_long`, `<field>_required` (a field sent as a list or an object) or `validation_error` (with an `errors` list).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "Get a recording",
        "description": "One recording in its detail form (`detail: true`), with a signed media\nURL good for one hour. A recording in the recycle bin answers only its\nowner, in the list form (no media URL, no share list); anyone else gets\n404 `recording_not_found`.\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The recording.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Recorder"
        ],
        "summary": "Update a recording",
        "description": "Changes a recording's title, description, call to action, visibility or\ndownload setting. The caller must be able to edit the recording; only its\nowner may change `allow_download`. A recording in the recycle bin cannot\nbe edited (403 `recording_deleted`).\n\nLeaving `anyone` visibility closes the recording's automatic public link:\nthe URL never works again (a later `anyone` makes a new one). The answer\nthen carries `public_link_closed: true` and a sentence saying so.\n\n**Token scopes:** `write:recorder`, and also `destructive:recorder` when\nthe request moves `visibility` away from `anyone` while the automatic\npublic link is live — closing that link is a final act (owner decision\n2026-10-07). Any other update needs only the write scope.\n\n`PUT` is accepted as an alias of `PATCH`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send only the fields to change. Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 150,
                    "description": "Trimmed and cut to 150 characters."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Cut to 5,000 characters."
                  },
                  "cta_label": {
                    "type": "string",
                    "description": "The call-to-action button's label; blank removes it."
                  },
                  "cta_url": {
                    "type": "string",
                    "description": "The call-to-action link; blank removes it."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "company",
                      "anyone"
                    ],
                    "description": "`anyone` needs public links turned on for the workspace (422 `public_links_disabled`) and the right to share externally (403 `share_externally_not_permitted`)."
                  },
                  "allow_download": {
                    "type": "boolean",
                    "description": "Owner only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recording was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    },
                    "public_link_closed": {
                      "type": "boolean",
                      "description": "Present when this change closed the automatic public link."
                    },
                    "public_link_closed_message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\", or \"Required scope: destructive:recorder\" when the change closes the public link), `app_not_enabled`, `app_not_accessible`, `forbidden` (the caller cannot edit the recording, or is not its owner for `allow_download`), `recording_deleted` or `share_externally_not_permitted`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "422": {
            "description": "`invalid_visibility`, `public_links_disabled`, `<field>_required` (a field sent as a list or an object) or `validation_error` (with an `errors` list).",
            "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": [
          "Recorder"
        ],
        "summary": "Delete a recording",
        "description": "Moves the caller's own recording to the recycle bin; it can be restored\nuntil it is purged (`days_until_purge`). Deleting a recording already in\nthe recycle bin answers the same success.\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recorder` scope; `write:recorder` is not enough. A token\nwithout it is refused with 403 `insufficient_permissions` (\"Required\nscope: destructive:recorder\").\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The recording is in the recycle bin.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording",
                    "deleted"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    },
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible` or `forbidden` (\"Only the owner can do that.\").",
            "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": "`recording_not_found`, or `recording_gone` (the recording was removed while this request ran).",
            "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": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          }
        }
      }
    },
    "/recorder/recordings/{id}/status": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "Poll a recording's processing status",
        "description": "The small answer a client polls while a capture is processed\n(`uploading` → `processing` → `ready` or `failed`). The media and poster\nURLs are signed for one hour and are null for a recording in the\nrecycle bin, which answers only its owner (anyone else gets 404).\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The recording's status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording_id",
                    "status"
                  ],
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "uploading",
                        "processing",
                        "ready",
                        "failed"
                      ]
                    },
                    "processing_stage": {
                      "type": "string",
                      "nullable": true
                    },
                    "error_message": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set only when `status` is `failed`."
                    },
                    "url": {
                      "type": "string",
                      "nullable": true,
                      "description": "Signed media URL, once the recording is ready."
                    },
                    "thumbnail_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "web_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "duration_seconds": {
                      "type": "number",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      }
    },
    "/recorder/recordings/{id}/restore": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Restore a recording from the recycle bin",
        "description": "Takes a recording out of the recycle bin. Its owner may restore it, and\nso may a Recorder admin (for any recording in the bin). Restoring one\nthat is not in the bin answers 200 with `restored: false`. A capture\nwhose upload never finished comes back as `failed`, ready to be\nuploaded again.\n\nAn admin restoring a recording they cannot watch gets it back in the\nrecycle-bin shape only.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The recording is out of the recycle bin.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording",
                    "restored"
                  ],
                  "properties": {
                    "recording": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/RecorderRecording"
                        },
                        {
                          "$ref": "#/components/schemas/RecorderRecycleBinRecording"
                        }
                      ]
                    },
                    "restored": {
                      "type": "boolean",
                      "description": "False when the recording was not in the recycle bin."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled` or `app_not_accessible`.",
            "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": "`recording_not_found` (no such recording the caller may restore), or `recording_gone` (it was purged while this request ran).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/transcript": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "Get a recording's transcript",
        "description": "The transcript segments for the native player's transcript panel, at\nmost 10,000 of them. A recording with no transcript, or one in the\nrecycle bin (which answers only its owner), answers an empty list.\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording_id",
                    "language",
                    "has_transcript",
                    "segments"
                  ],
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "language": {
                      "type": "string",
                      "description": "The recording's own language code (`en` when unknown)."
                    },
                    "has_transcript": {
                      "type": "boolean",
                      "description": "The recording has a transcript — the same answer as the detail's `has_transcript`. Can be true with no `segments` when the text exists but no timed segment parsed; show `text`."
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecorderTranscriptSegment"
                      }
                    },
                    "segments_truncated": {
                      "type": "boolean",
                      "description": "The transcript has more segments than the 10,000 `segments` carries."
                    },
                    "segment_count": {
                      "type": "integer",
                      "description": "How many timed segments the whole transcript has."
                    },
                    "text": {
                      "type": "string",
                      "nullable": true,
                      "description": "The whole transcript as plain text."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      }
    },
    "/recorder/recordings/{id}/captions": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "Get a recording's captions",
        "description": "The WebVTT captions the web player's track uses, as JSON, so a\nbearer-authenticated client can play them. With `lang`, a ready caption\ntrack in that language, or the transcript when `lang` is the\nrecording's own language; without it, the transcript. `languages`\nlists what is on offer.\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "A language code (`en`, `pt-BR`) or an English language name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The captions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording_id",
                    "language",
                    "languages",
                    "vtt"
                  ],
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "language": {
                      "type": "string",
                      "description": "The language of `vtt`."
                    },
                    "languages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecorderCaptionLanguage"
                      }
                    },
                    "vtt": {
                      "type": "string",
                      "description": "A WebVTT document."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": "`recording_not_found`, or `captions_not_found` (no captions in that language — also the answer for a recording in the recycle bin).",
            "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": "`lang_required` — `lang` was sent as a list or 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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/media/init": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Start a direct media upload",
        "description": "The first step of the direct upload: checks the declared file and\nanswers a storage URL to `PUT` the bytes to, with the headers to send.\nThen call `media/complete` with `upload.signed_blob_id`. The URL\nexpires (`expires_in`, `expires_at`); a repeated init mints a fresh one.\nOnly the recording's owner may upload its media, and not while it is in\nthe recycle bin.\n\nThe `Idempotency-Key` header is not honoured here, on purpose: a\nreplayed answer would carry an expired URL.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "content_type",
                  "byte_size",
                  "checksum"
                ],
                "description": "Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "Cut to 180 characters, keeping the extension."
                  },
                  "content_type": {
                    "$ref": "#/components/schemas/RecorderMediaType"
                  },
                  "byte_size": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "At most the workspace's upload cap (`settings.max_upload_size_mb` in `GET /recorder/config`)."
                  },
                  "checksum": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Base64 MD5 of the bytes (24 characters)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where to send the bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording_id",
                    "upload",
                    "limits"
                  ],
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "upload": {
                      "type": "object",
                      "properties": {
                        "signed_blob_id": {
                          "type": "string",
                          "description": "Good only for this recording's `media/complete`."
                        },
                        "url": {
                          "type": "string",
                          "description": "The storage URL to `PUT` the bytes to."
                        },
                        "headers": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "Headers to send with the `PUT`."
                        },
                        "byte_size": {
                          "type": "integer"
                        },
                        "content_type": {
                          "type": "string"
                        },
                        "declared_content_type": {
                          "type": "string"
                        },
                        "expires_in": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Seconds until `url` expires."
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    },
                    "limits": {
                      "$ref": "#/components/schemas/RecorderUploadLimits"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `forbidden` (\"Only the owner can do that.\") or `recording_deleted`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "413": {
            "$ref": "#/components/responses/RecorderMediaTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/RecorderMediaTypeUnsupported"
          },
          "422": {
            "description": "`upload_metadata_required` (a field is missing, or `byte_size` is not above zero), `checksum_too_long` or `<field>_required`.",
            "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"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "`upload_url_unavailable` — storage could not issue an upload URL; retry.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/media/complete": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Finish a direct media upload",
        "description": "Attaches the uploaded bytes to the recording and starts processing. The\n`signed_blob_id` comes from `media/init`, or — for a capture too large\nfor one request — from finishing an upload session (`POST\n/uploads/sessions` with purpose `recorder`). The bytes are virus-scanned\nand their real type checked here. Only the recording's owner may call\nit, and not while it is in the recycle bin.\n\nA retry with the same bytes keeps everything already derived from them.\nNew bytes replace the media and clear what came from the old ones: the\ntranscript, summary, chapters, caption tracks and annotations bundle.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "signed_blob_id"
                ],
                "properties": {
                  "signed_blob_id": {
                    "type": "string",
                    "description": "A single value (422 `signed_blob_id_required` for a list or an object)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/RecorderProcessingStarted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `forbidden` (\"Only the owner can do that.\") or `recording_deleted`.",
            "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": "`recording_not_found`, or `blob_not_found` (no such upload this caller may attach here).",
            "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": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/RecorderMediaTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/RecorderMediaTypeUnsupported"
          },
          "422": {
            "description": "`blob_id_required`, `signed_blob_id_required`, `upload_metadata_required` (the upload is empty), `upload_incomplete` (the bytes never finished arriving, or do not match the declared checksum — start a new upload), `upload_blocked` (the virus scan refused the file; nothing was kept) 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"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Worth retrying: `scanner_unavailable` (virus scanning is down and this workspace refuses uploads meanwhile), `storage_unavailable` (file storage could not be read) or `processing_unavailable` (the bytes are attached but processing could not be started; the recording is marked `failed` — send `media/complete` again).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/media": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Upload media in one request (multipart)",
        "description": "The small-file fallback for `media/init` + `media/complete`: the\ncapture in a multipart `file` field. Same checks and outcome as\n`media/complete` — owner only, virus-scanned, the real type checked,\nprocessing started — and the same rule for a retry with the same bytes.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The capture (WebM, MP4 or QuickTime video, or a PNG or JPEG screenshot)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/RecorderProcessingStarted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `forbidden` (\"Only the owner can do that.\") or `recording_deleted`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/RecorderMediaTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/RecorderMediaTypeUnsupported"
          },
          "422": {
            "description": "`file_required`, `upload_metadata_required` (the file is empty), `upload_blocked` (the virus scan refused it; nothing was kept), `upload_incomplete` 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"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "`upload_storage_unavailable` — storage refused the bytes; nothing changed on the recording. Send the file again.",
            "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": "`scanner_unavailable` (virus scanning is down; nothing was stored) or `processing_unavailable` (the bytes are attached but processing could not be started; the recording is marked `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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/thumbnail": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Set a recording's poster image",
        "description": "Replaces the recording's poster with an image the client supplies, in a\nmultipart `file` field: PNG, JPEG or WebP, at most 2 MB. Both the\ndeclared type and the bytes themselves must be one of those. The caller\nmust be able to edit the recording, and it must not be in the recycle\nbin.\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "A PNG, JPEG or WebP image, at most 2 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The poster was replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `forbidden` (the caller cannot edit the recording) or `recording_deleted`.",
            "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": "`recording_not_found`, or `recording_gone` (the recording was removed while this request ran).",
            "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": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "413": {
            "description": "`file_too_large` — over 2 MB.",
            "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"
                  ]
                }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type` — not a PNG, JPEG or WebP (declared or by its bytes); `error.details.allowed_types` lists them.",
            "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": "`file_required`, `upload_metadata_required` (the file is empty) 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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/send_to_chat": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Send a recording to a chat room",
        "description": "Posts a card with the recording's title, an optional note and its link\ninto a chat room the caller is a member of and may post in. The\nmessage is queued (202). The same card into the same room within ten\nminutes is treated as a retry and answers 200 `duplicate` with the\nmessage already posted.\n\n`link: company` (the default) sends the link for signed-in colleagues;\n`link: anyone` sends a public link (made if needed), which needs public\nlinks turned on for the workspace, the caller able to edit the\nrecording, and the caller allowed to share externally.\n\nGates after Recorder's: the token's `write:chat`, then the Chat app\n(403 `chat_not_enabled` / `chat_not_accessible`, with Chat's message —\nChat's own codes, so they are never read as Recorder being off). A\ncolleague's recording in the recycle bin answers 404, as the reads do;\nonly its owner hears `recording_deleted`.\n\n**Token scopes:** `write:recorder` and `write:chat`. A token holding\nonly `write:own_chat` passes, but may not post as a business admin\ninto a room only admins may post in.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room_id"
                ],
                "description": "Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "room_id": {
                    "type": "integer",
                    "description": "The chat room (422 `room_id_required` when missing)."
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Trimmed and cut to 1,000 characters."
                  },
                  "link": {
                    "type": "string",
                    "enum": [
                      "company",
                      "anyone"
                    ],
                    "default": "company",
                    "description": "Any other value sends the company link."
                  },
                  "client_uuid": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The client's own id for the chat message."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The card is queued for the room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderSentToChat"
                }
              }
            }
          },
          "200": {
            "description": "The same card was already sent to this room in the last ten minutes; nothing new was posted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderSentToChat"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\" or \"Required scope: write:chat\"), `app_not_enabled` / `app_not_accessible` (Recorder), `chat_not_enabled` / `chat_not_accessible` (Chat), `recording_deleted` (the owner's own recording is in the recycle bin), `forbidden` (not a member of the room; an `anyone` link from someone who cannot edit the recording or may not share externally), `im_not_allowed` or `information_barrier` (the caller may not post in this room).",
            "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": "`recording_not_found`, or `room_not_found` (no such live chat room in this workspace).",
            "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": "`recently_sent` (this card was sent to the room moments ago and then deleted; wait a few minutes), `idempotency_conflict` or `idempotency_in_progress`.",
            "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": "`room_id_required`, `public_links_disabled` (send the company link instead), `client_uuid_too_long`, `message_too_long` or `<field>_required`.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/watch": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Report what a signed-in viewer watched",
        "description": "The player's engagement beacon. The first beacon counts a play (one per\nviewer per 30 minutes); every beacon merges its watched ranges into\nthat play, which feeds the owner's completion rate, drop-off and\nheatmap. The owner watching their own recording is not counted (still\n204).\n\n**Token scopes:** `write:recorder`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ranges": {
                    "type": "array",
                    "description": "Watched spans as `[from_seconds, to_seconds]` pairs (also accepted as that array in a JSON string). At most 200 are read; a pair that is not two numbers with `from` ≥ 0 and `to` > `from` is dropped, and spans are clipped to the recording's length.",
                    "maxItems": 200,
                    "items": {
                      "type": "array",
                      "minItems": 2,
                      "maxItems": 2,
                      "items": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Recorded (or the caller is the owner, which is not counted)."
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible` or `recording_not_ready` (the recording is not ready to play, or the caller's own recording is in the recycle bin — a colleague's answers 404).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "503": {
            "description": "`view_not_recorded` — the view could not be saved; send the beacon again.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/share_links": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "List a recording's public links",
        "description": "The recording's public links that have not been revoked, newest first\n(an expired or used-up link is listed with its `status`). The caller\nmust be able to edit the recording — a link's URL is the credential\nfor outsiders — and it must not be in the recycle bin.\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderCursor"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of public links.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "share_links",
                    "total",
                    "next_cursor",
                    "public_links_enabled"
                  ],
                  "properties": {
                    "share_links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecorderShareLink"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Links not revoked, across every page."
                    },
                    "next_cursor": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The `cursor` (an offset) for the next page; null on the last page."
                    },
                    "public_links_enabled": {
                      "type": "boolean",
                      "description": "Whether the workspace allows public links at all."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled`, `app_not_accessible`, `recording_deleted` (in the recycle bin) or `forbidden` (the caller cannot edit the recording).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Create a public link",
        "description": "Makes a public \"anyone with the link\" URL for the recording. Needs\npublic links turned on for the workspace, the caller able to edit the\nrecording and allowed to share externally, and the recording out of the\nrecycle bin.\n\nExpiry follows the workspace's `public_link_max_days`: above 0, a blank\n`expires_at` gets that many days and a later date is pulled back to the\ncap; at 0 (no limit), a blank one never expires. A date given must be in\nthe future (and, without a cap, within 100 years); a bare date means the\nend of that day.\n\n**Token scopes:** `write:recorder` (revoking a link asks\n`destructive:recorder`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Every field is optional. Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Trimmed and cut to 80 characters."
                  },
                  "expires_at": {
                    "type": "string",
                    "description": "A date or a date-time (422 `invalid_expiry` when unparseable or not in the future)."
                  },
                  "password": {
                    "type": "string",
                    "description": "Asked of anyone opening the link. At most 72 bytes (422 `validation_error` beyond)."
                  },
                  "max_views": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Opens allowed; above 100,000 is cut to 100,000. Leave out for unlimited (422 `invalid_max_views` for zero, a negative or a non-number)."
                  },
                  "require_email": {
                    "type": "boolean",
                    "default": false,
                    "description": "Ask a viewer for their email before playing."
                  },
                  "allow_download": {
                    "type": "boolean",
                    "default": false,
                    "description": "Offer a download through the link; it works only while the recording itself allows downloads."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link was created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "share_link"
                  ],
                  "properties": {
                    "share_link": {
                      "$ref": "#/components/schemas/RecorderShareLink"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted`, `forbidden` (the caller cannot edit the recording) or `share_externally_not_permitted` (the workspace limits who may publish on a public link).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "422": {
            "description": "`public_links_disabled` (turned off for this workspace), `invalid_expiry`, `invalid_max_views`, `<field>_required` or `validation_error` (with an `errors` list).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/chat_rooms/{room_id}": {
      "delete": {
        "tags": [
          "Recorder"
        ],
        "summary": "Stop a chat room watching a recording",
        "description": "The web Access card's Stop: a chat room the recording was sent to can no\nlonger watch it. The card stays in the room. The caller must be able to\nedit the recording and to manage who it is shared with\n(`can_manage_sharing`). The answer lists the rooms still open, as\n`shared_with_rooms` on the recording does.\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): `write:recorder` is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "description": "Chat room ID (`shared_with_rooms[].room_id`)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The room can no longer watch it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "shared_with_rooms": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecorderSharedRoom"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted`, `forbidden` (the caller cannot edit the recording) or `resharing_not_permitted`.",
            "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": "`recording_not_found`, or `chat_room_not_found` (\"That chat room can't watch this recording.\").",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/comments": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "List a recording's comments",
        "description": "The comment thread, oldest first: top-level comments, each with up to\n50 of its replies (`reply_count` and `replies_truncated` say when there\nare more). Anyone who can watch the recording may read it; a recording\nin the recycle bin answers only its owner.\n\nThe cursor is an offset over top-level comments, so a page always\nholds whole threads. `total` counts top-level comments;\n`comments_count` counts every live comment including replies (the\nnumber on the recording's card).\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderCursor"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Top-level comments per page, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the thread.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording_id",
                    "can_comment",
                    "comments",
                    "total",
                    "comments_count",
                    "next_cursor"
                  ],
                  "properties": {
                    "recording_id": {
                      "type": "integer"
                    },
                    "can_comment": {
                      "type": "boolean",
                      "description": "The recording is out of the recycle bin and the token carries `write:recorder`."
                    },
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecorderComment"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Top-level comments, across every page."
                    },
                    "comments_count": {
                      "type": "integer",
                      "description": "Every live comment, replies included."
                    },
                    "next_cursor": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The `cursor` (an offset) for the next page; null on the last page."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled` or `app_not_accessible`.",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Comment on a recording",
        "description": "Posts a comment, optionally pinned to a moment in the recording or as a\nreply. Anyone who can watch the recording may comment, while it is out\nof the recycle bin. The comment is screened by the workspace's content\nmoderation before it is saved. The recording's owner and anyone\n@mentioned are notified.\n\nThreads are one level deep: a reply to a reply is filed under the\ntop-level comment.\n\n**Token scopes:** `write:recorder` (deleting a comment asks\n`destructive:recorder`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "description": "Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "Trimmed; at most 4,000 characters (422 `body_too_long`, never cut). An @mention counts at its stored length, which is longer than the name shown."
                  },
                  "anchor_seconds": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "A moment in the recording, in seconds. Ignored when the recording has no length or the moment is past its end."
                  },
                  "parent_comment_id": {
                    "type": "integer",
                    "description": "The comment this replies to."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The comment was posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "comment"
                  ],
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/RecorderComment"
                    },
                    "mentions_capped": {
                      "type": "integer",
                      "description": "Present when the notification cap left this many mentioned people untold."
                    },
                    "mentions_unreachable": {
                      "type": "integer",
                      "description": "Present when this many mentioned people were not told because they cannot open the recording."
                    },
                    "mentions_unreachable_message": {
                      "type": "string",
                      "description": "The sentence to show beside `mentions_unreachable`."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible` or `recording_deleted` (in the recycle bin).",
            "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": "`recording_not_found`, or `parent_not_found` (\"That comment is gone already.\").",
            "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": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "422": {
            "description": "`body_required`, `body_too_long`, `content_moderation_blocked` (the message says why), `comment_invalid`, `<field>_required` or `validation_error` (with an `errors` list).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/shares": {
      "get": {
        "tags": [
          "Recorder"
        ],
        "summary": "List who a recording is shared with",
        "description": "The people a recording is shared with, oldest grant first. The caller\nmust be able to edit the recording, and it must not be in the recycle\nbin. The owner is not listed (they always have full access).\n\n**Token scopes:** none — Recorder reads are not scope-gated.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderCursor"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the share list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderShareRoster"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`app_not_enabled`, `app_not_accessible`, `recording_deleted` (in the recycle bin) or `forbidden` (the caller cannot edit the recording).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Share a recording with a person",
        "description": "Gives one person `viewer` or `editor` access. The caller must be able to\nedit the recording and to manage who it is shared with (its owner, or\nan editor while the workspace lets editors re-share), and it must not be\nin the recycle bin. The person must be an active member of the\nworkspace who can open Recorder.\n\nFor a person not yet on the list, an unrecognised `permission` grants\n`viewer` (201). For a person already on it, this changes their access:\n`permission` is then required and must be `viewer` or `editor` (200,\n`existing: true`). The answer is the share list as it now stands (first\npage).\n\n**Token scopes:** `write:recorder` (removing a person asks\n`destructive:recorder`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "description": "Each must be a single value (422 `<field>_required` for a list or an object).",
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "description": "The person to share with."
                  },
                  "permission": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor"
                    ],
                    "description": "The owner's access is the creator's and cannot be granted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The person was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderShareRosterAfterShare"
                }
              }
            }
          },
          "200": {
            "description": "The person was already on the list; their access was set to `permission`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderShareRosterAfterShare"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted`, `forbidden` (the caller cannot edit the recording) or `resharing_not_permitted` (\"Only the person who made this recording can choose who it is shared with.\").",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "422": {
            "description": "`user_not_grantable` (not an active member who can open Recorder), `owner_already` (that person made the recording), `permission_required` or `invalid_permission` (a person already on the list, with `permission` missing or not `viewer`/`editor`), `user_id_required` (sent as a list or an object), or `validation_error` (the share could not be saved; `errors` says why).",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{id}/annotations": {
      "post": {
        "tags": [
          "Recorder"
        ],
        "summary": "Save a screenshot's annotations",
        "description": "Stores the desktop editor's annotations bundle on a screenshot, so it\ncan be edited again later from any desktop. The bundle is the\n`<file>.annotations.json` document the desktop writes, sent as-is in a\nmultipart `file` field, at most 60 MB. The screenshot's media must be\nuploaded first. The caller must be able to edit the recording, and it\nmust not be in the recycle bin. Editors read the bundle back from\n`annotations_url` on the recording's detail.\n\n**Token scopes:** `write:recorder` (removing the annotations asks\n`destructive:recorder`).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          },
          {
            "$ref": "#/components/parameters/RecorderIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "A JSON document with `format: \"mangoapps-desktop/annotations\"`, an integer `version`, an `ops` array and an `original` whose `dataUrl` is an image data URL. At most 60 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The annotations were saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted` (in the recycle bin) or `forbidden` (the caller cannot edit the recording).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RecorderIdempotencyConflict"
          },
          "413": {
            "description": "`file_too_large` — over 60 MB.",
            "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": "`file_required`, `not_a_screenshot` (only a screenshot carries annotations), `media_required` (upload the screenshot first), `upload_metadata_required` (the file is empty), `invalid_annotations_bundle` (not an annotations bundle) 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"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Recorder"
        ],
        "summary": "Remove a recording's annotations",
        "description": "Removes the drawn annotations from a recording the caller can edit. The\nstored file is purged in the background.\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recorder` scope; `write:recorder` (which saving annotations\nasks) is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderRecordingId"
          }
        ],
        "responses": {
          "200": {
            "description": "The annotations were removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recording"
                  ],
                  "properties": {
                    "recording": {
                      "$ref": "#/components/schemas/RecorderRecording"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted` (in the recycle bin) or `forbidden` (the caller cannot edit the recording).",
            "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": {
            "$ref": "#/components/responses/RecorderRecordingNotFound"
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/comments/{comment_id}": {
      "delete": {
        "tags": [
          "Recorder"
        ],
        "summary": "Delete a comment",
        "description": "Deletes a comment on a recording together with its replies (and their\nreplies). A comment's author may delete it, and so may the recording's\nowner.\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recorder` scope; `write:recorder` is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "recording_id",
            "in": "path",
            "required": true,
            "description": "Recording ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "comment_id",
            "in": "path",
            "required": true,
            "description": "Comment ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comment and its replies were deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "comment_id",
                    "deleted_reply_ids"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "example": true
                    },
                    "comment_id": {
                      "type": "integer"
                    },
                    "deleted_reply_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted` (in the recycle bin) or `forbidden` (\"You can only delete your own comments.\").",
            "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": "`recording_not_found`, or `comment_not_found` (\"That comment is gone already.\").",
            "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": "`comment_delete_failed` — the comment could not be deleted; try again.",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/shares/{share_id}": {
      "patch": {
        "tags": [
          "Recorder"
        ],
        "summary": "Change a person's access to a recording",
        "description": "Sets one share's `permission`. The caller must be able to edit the\nrecording and to manage who it is shared with, and it must not be in\nthe recycle bin. The answer is the share list as it now stands (first\npage).\n\n**Token scopes:** `write:recorder`.\n\n`PUT` is accepted as an alias of `PATCH`.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RecorderParentRecordingId"
          },
          {
            "name": "share_id",
            "in": "path",
            "required": true,
            "description": "Share ID (`shares[].id`)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "permission"
                ],
                "properties": {
                  "permission": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor"
                    ],
                    "description": "Required; a single value."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The access was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderShareRoster"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: write:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted`, `forbidden` (the caller cannot edit the recording) or `resharing_not_permitted`.",
            "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": "`recording_not_found`, or `share_not_found` (\"That share no longer exists.\").",
            "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": "`permission_required` (missing, or sent as a list or an object), `invalid_permission` (not `viewer` or `editor`; `error.details.permission` echoes what was sent) or `validation_error` (the change could not be saved; `errors` says why).",
            "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": [
          "Recorder"
        ],
        "summary": "Stop sharing a recording with a person",
        "description": "Removes one person from a recording's share list. The caller must be able\nto edit the recording and to manage who it is shared with. The answer is\nthe share list as it now stands (first page).\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recorder` scope; `write:recorder` (which adding or changing\na share asks) is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "recording_id",
            "in": "path",
            "required": true,
            "description": "Recording ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "share_id",
            "in": "path",
            "required": true,
            "description": "Share ID (`shares[].id`)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The person was removed from the share list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecorderShareRoster"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted`, `forbidden` (the caller cannot edit the recording) or `resharing_not_permitted` (\"Only the person who made this recording can choose who it is shared with.\").",
            "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": "`recording_not_found`, or `share_not_found` (\"That share no longer exists.\").",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recorder/recordings/{recording_id}/share_links/{id}": {
      "delete": {
        "tags": [
          "Recorder"
        ],
        "summary": "Revoke a public link",
        "description": "Revokes one of a recording's live public links; its URL stops working.\nThe caller must be able to edit the recording.\n\n**Required Scopes:** `destructive:recorder`\n\nDelete-level (owner decision 2026-10-04, #6721): requires the\n`destructive:recorder` scope; `write:recorder` (which creating a link\nasks) is not enough.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "recording_id",
            "in": "path",
            "required": true,
            "description": "Recording ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Public link ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The link was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "share_link",
                    "revoked"
                  ],
                  "properties": {
                    "share_link": {
                      "$ref": "#/components/schemas/RecorderShareLink"
                    },
                    "revoked": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_permissions` (\"Required scope: destructive:recorder\"), `app_not_enabled`, `app_not_accessible`, `recording_deleted` or `forbidden` (the caller cannot edit the recording).",
            "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": "`recording_not_found`, or `share_link_not_found` (\"That public link no longer exists.\").",
            "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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/recruiting/candidates/{id}": {
      "patch": {
        "tags": [
          "Recruiting"
        ],
        "summary": "Update a candidate",
        "description": "Moves a candidate to another pipeline status and/or replaces their\nnotes. Rejecting a candidate records the reason on their timeline and,\nwhen the business sends rejection emails, queues one\n(`rejection_email_queued`).\n\nA candidate who was closed out cannot be moved back into the pipeline\nhere (422 `candidate_archived` — reopen them in the web app). `hired`\nneeds an accepted offer and `offered` an offer in play (422\n`offer_required`).\n\n**Token scopes:** `write:recruiting`, and also `destructive:recruiting`\nwhen `candidate.status` is `rejected`, `withdrawn` or `unsuitable` —\nclosing a candidate out is a final act (owner decision 2026-10-04,\n#6721). Any other update needs only the write scope.\n\n`PUT` is accepted as an alias of `PATCH`. An `Idempotency-Key` header\nmakes a retry return the first success instead of writing again.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Candidate ID",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A key you choose per write; a retry with the same key replays the stored success.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "candidate"
                ],
                "properties": {
                  "candidate": {
                    "type": "object",
                    "properties": {
                      "status": {
                        "type": "string",
                        "enum": [
                          "applied",
                          "screening",
                          "interviewed",
                          "ready_for_offer",
                          "offered",
                          "hired",
                          "rejected",
                          "withdrawn",
                          "qualified",
                          "potentially_qualified",
                          "unsuitable",
                          "interviews_in_process"
                        ],
                        "description": "`rejected`, `withdrawn` and `unsuitable` also need `destructive:recruiting`. Any other value is refused (422 `invalid_status`)."
                      },
                      "notes": {
                        "type": "string"
                      },
                      "rejection_reason": {
                        "type": "string",
                        "description": "Recorded when present."
                      },
                      "rejection_category": {
                        "type": "string",
                        "description": "Recorded together with `rejection_reason`."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The candidate was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "candidate"
                  ],
                  "properties": {
                    "candidate": {
                      "$ref": "#/components/schemas/RecruitingCandidateSummary"
                    },
                    "rejection_email_queued": {
                      "type": "boolean",
                      "description": "Present when this request rejected the candidate."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_payload` — \"candidate must be an object\" (the body has no `candidate` object, or it is 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": "`recruiting_not_enabled`, `insufficient_permissions` (no recruiting access, or the token lacks `write:recruiting` — or `destructive:recruiting` for a close-out) or `forbidden` (\"You do not have permission to update this candidate\").",
            "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` — \"Candidate not found\" 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": "`invalid_status`, `candidate_archived`, `offer_required` or `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"
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "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": {
      "ChangeRequestId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "AccessRequestId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "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
        }
      },
      "NotepadMeetingId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Meeting id.",
        "schema": {
          "type": "integer"
        }
      },
      "NotepadMeetingIdPath": {
        "name": "meeting_id",
        "in": "path",
        "required": true,
        "description": "Meeting id.",
        "schema": {
          "type": "integer"
        }
      },
      "NotepadSpeakerId": {
        "name": "speaker_id",
        "in": "path",
        "required": true,
        "description": "A roster speaker id — `me`, `them`, `them_a`, `s1`.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9_]{1,12}$"
        }
      },
      "AppointmentSchedulerAppointmentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "ChatThreadsParam": {
        "name": "threads",
        "in": "query",
        "description": "The client draws channel threads (D1). In a channel the window is the\ntimeline the web shows — everything except replies kept in their thread\n— and each root carries `thread` (reply count, last reply, repliers,\nfollowing). In a DM or group every message comes back, replies as inline\nquotes. Wins over `include_thread_replies`. Without it nothing changes\nfor an older client: a thread reply still arrives inline with\n`include_thread_replies=true`.\n",
        "schema": {
          "type": "boolean",
          "default": false
        }
      },
      "CommsHubPerPage": {
        "name": "per_page",
        "in": "query",
        "description": "Rows per page. Clamped to 1..100; a blank value means 25, and a non-numeric value reads as 0 and is clamped to 1.",
        "schema": {
          "type": "integer",
          "default": 25,
          "minimum": 1,
          "maximum": 100
        }
      },
      "CommsHubIssueLang": {
        "name": "lang",
        "in": "query",
        "description": "Language to return a sent issue in (e.g. `es`, `fr-CA`). Falls back to the original (`en`) when the issue has no reviewed translation in that language. Ignored for an issue that has not been sent.",
        "schema": {
          "type": "string"
        }
      },
      "CompanyStoreCheckoutOrderNumber": {
        "name": "order_number",
        "in": "path",
        "required": true,
        "description": "The ORDER NUMBER (e.g. `ORD-20260819-ABC123`) from the checkout write's response — the same identifier the web URLs and the orders endpoints use, not the numeric id and not the Stripe session id.",
        "schema": {
          "type": "string"
        }
      },
      "ExportFilesId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Export file ID",
        "schema": {
          "type": "integer"
        }
      },
      "IntelligenceAssistantType": {
        "name": "assistant_type",
        "in": "query",
        "description": "Which AI assistant to tailor to. `task_assistant`, `form_assistant`, `database_assistant` and `admin_assistant` add their own keys; any other value is accepted and adds none.",
        "schema": {
          "type": "string",
          "default": "task_assistant",
          "example": "task_assistant"
        }
      },
      "MangoSignalAggregateWindowDays": {
        "name": "window_days",
        "in": "query",
        "required": false,
        "description": "Days of signals to read, 1–90 (default 30); the applied value is echoed.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 90
        }
      },
      "MangoSignalRowWindowDays": {
        "name": "window_days",
        "in": "query",
        "required": false,
        "description": "Days to read, 1–365; no window when omitted. The applied value is echoed.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 365
        }
      },
      "MealComplianceWaiverId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "The waiver's id — `items[].id` from `GET /meal_compliance/waivers`."
      },
      "MealComplianceIdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Send one per press and repeat it on a retry: a repeated key answers the first response instead of acting twice. Resend signing link in particular has no state that stops a second reminder email."
      },
      "MealComplianceLocale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "example": "es"
        },
        "description": "The language for the words this response writes. Omitted, the Accept-Language header decides, then the caller's own language."
      },
      "MuscleMemoryLessonId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The lesson's id (`lesson.id`). An id that does not exist in this business, belongs to a sample lesson, or — for the read — to a forgotten one answers `404 not_found`.",
        "schema": {
          "type": "integer"
        }
      },
      "PracticeHubPage": {
        "name": "page",
        "in": "query",
        "required": false,
        "description": "Page number, from 1. A missing, non-numeric or non-positive value means page 1; a page past the end returns an empty list.",
        "schema": {
          "type": "integer",
          "default": 1
        }
      },
      "PracticeHubPerPage": {
        "name": "per_page",
        "in": "query",
        "required": false,
        "description": "Rows per page, at most 100 (a larger value is lowered to 100). A missing, blank, non-numeric, zero or negative value means 25.",
        "schema": {
          "type": "integer",
          "default": 25,
          "minimum": 1,
          "maximum": 100
        }
      },
      "RecorderRecordingId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Recording ID",
        "schema": {
          "type": "integer"
        }
      },
      "RecorderParentRecordingId": {
        "name": "recording_id",
        "in": "path",
        "required": true,
        "description": "Recording ID",
        "schema": {
          "type": "integer"
        }
      },
      "RecorderIdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A key you choose per write; a retry with the same key replays the first answer.",
        "schema": {
          "type": "string"
        }
      },
      "RecorderCursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "The previous page's `next_cursor`. Leave out for the first page.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "SitesApprovalPageId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The page's id — the Approvals row's `id`.",
        "schema": {
          "type": "integer"
        }
      },
      "SitesCarePageId": {
        "name": "page_id",
        "in": "path",
        "required": true,
        "description": "The page's id.",
        "schema": {
          "type": "integer"
        }
      },
      "SitesSiteId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The site's id.",
        "schema": {
          "type": "integer"
        }
      },
      "SopId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "ServiceDeskProblemError": {
        "description": "Refused",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "error": {
                  "type": "object",
                  "description": "`code` is the code for the response status (400 `invalid_request`, 403 `forbidden`, 404 `not_found`, 409 `conflict`, 422 `validation_failed`, 503 `service_unavailable`); `message` is the sentence.",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "ChangeRequestActionDone": {
        "description": "Done — the change request as it now stands",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": true
                },
                "message": {
                  "type": "string",
                  "example": "Change request submitted for review."
                },
                "warnings": {
                  "type": "array",
                  "description": "Caveats — a warn-mode blackout overlap, or a timeline entry that could not be written",
                  "items": {
                    "type": "string"
                  }
                },
                "outcome": {
                  "type": "string",
                  "nullable": true,
                  "enum": [
                    "approved",
                    "rejected"
                  ],
                  "description": "For a vote — set when this ballot carried the change"
                },
                "change_request": {
                  "$ref": "#/components/schemas/ChangeRequestDetail"
                }
              }
            }
          }
        }
      },
      "ChangeRequestActionRefused": {
        "description": "403 — the caller may see the change but not take this action. 409 — the change is no longer\neditable. 422 — the change is in the wrong state (e.g. \"Invalid transition from draft to implementing\"),\nor the input failed validation.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "error": {
                  "type": "object",
                  "description": "`code` is `forbidden` (403), `conflict` (409) or `validation_failed` (422); `message` is the sentence.",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string",
                      "example": "Invalid transition from draft to implementing"
                    }
                  }
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "AccessRequestDecided": {
        "description": "Done",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": true
                },
                "message": {
                  "type": "string",
                  "example": "Access request has been rejected."
                },
                "access_request": {
                  "$ref": "#/components/schemas/AccessRequestDetail"
                }
              }
            }
          }
        }
      },
      "AccessRequestForbidden": {
        "description": "The caller can see this request but may not take this action",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccessRequestError"
            }
          }
        }
      },
      "AccessRequestDecisionFailed": {
        "description": "The action did not happen (e.g. the request is no longer in a state that allows it, or the grant failed); the current request is returned",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "error": {
                  "type": "object",
                  "description": "`code` is the code for the response status (400 `invalid_request`, 403 `forbidden`, 404 `not_found`, 409 `conflict`, 422 `validation_failed`, 503 `service_unavailable`); `message` is the sentence.",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "access_request": {
                  "$ref": "#/components/schemas/AccessRequestDetail"
                }
              }
            }
          }
        }
      },
      "CommunicationsUnavailable": {
        "description": "Communications is off for the caller's business (`app_disabled`), not\nopen to the caller — not published to them, or outside its visibility\nrules (`app_unavailable`) — or the admin's Communications access policy\nrefuses this work context (`work_context_restricted`). Checked after\nthe token scope and before anything else; nothing is read or written.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "app_disabled",
                        "app_unavailable",
                        "work_context_restricted",
                        "forbidden"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "Communications is not enabled for this business."
                    }
                  }
                },
                "code": {
                  "type": "string",
                  "description": "The same code",
                  "also at the top level.": null
                }
              }
            }
          }
        }
      },
      "ServiceDeskGrievanceError": {
        "description": "Refused",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "example": false
                },
                "error": {
                  "type": "object",
                  "description": "`code` is the code for the response status (400 `invalid_request`, 403 `forbidden`, 404 `not_found`, 409 `conflict`, 422 `validation_failed`, 503 `service_unavailable`); `message` is the sentence.",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "ServiceDeskGrievanceUpdated": {
        "description": "Done — the grievance as it now stands",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean"
                },
                "message": {
                  "type": "string"
                },
                "grievance": {
                  "$ref": "#/components/schemas/ServiceDeskGrievanceDetail"
                }
              }
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "NotModified": {
        "description": "Not Modified. The request carried `If-None-Match` and it matched the\nresponse's current ETag — the `ETag` header of the previous 200, which\nis also `_meta.http.caching.etag` in its body (weak and strong forms\ncompare equal). The body is empty: keep using the copy you already\nhold. A request without `If-None-Match`, or with a stale tag, gets the\nfull 200.\n"
      },
      "ImpersonationAccountSecurityForbidden": {
        "description": "Refused for an impersonation token (error code `impersonation_cannot_change_account_security`).\nAn impersonation session can act as the person for ordinary data access, but it can never change\nhow they sign in or recover their account. The person must make this change from their own sign-in.\nOnly impersonation tokens receive this refusal.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "impersonation_cannot_change_account_security",
                "message": "An impersonation session can't change sign-in details, passwords, two-factor authentication or account recovery settings; the person must make this change from their own sign-in."
              }
            }
          }
        }
      },
      "CredentialsWriteForbidden": {
        "description": "Refused. A token without the `write:connections` scope answers `insufficient_permissions`:\nminting, rotating and revoking a person's API tokens, signing out their sessions,\ndisconnecting their assistants and managing their two-factor authentication and backup\ncodes need it (the app's own sign-in and the CLI carry it; a token\nyou make carries it only when you grant it). An impersonation token answers\n`impersonation_cannot_change_account_security`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing_scope": {
                "value": {
                  "error": {
                    "code": "insufficient_permissions",
                    "message": "Required scope: write:connections"
                  }
                }
              },
              "impersonation": {
                "value": {
                  "error": {
                    "code": "impersonation_cannot_change_account_security"
                  }
                }
              }
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "ApiCreditsExhausted": {
        "description": "The organization has used its plan's monthly API allowance and has no API credits left\n(error code `api_credits_exhausted`). Answered to a call made with an API token someone\ncreated (personal, admin, service-account or CLI) until the allowance resets on the 1st,\ncredits are bought, or auto top-up buys more; never to the MangoApps apps' own sign-in or a\nsigned-in session.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "api_credits_exhausted",
                "message": "This account has used its plan's API allowance for the month and has no API credits left. Buy platform credits, or turn on auto top-up, under Admin → Billing & Payments → Credits; the allowance resets on the 1st.",
                "details": {
                  "buy_credits": "/admin/billing/purchase_credits?resource_type=api_calls"
                }
              }
            }
          }
        }
      },
      "NotepadForbidden": {
        "description": "The AI Notepad app is not enabled for this business, or not visible to the caller.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotepadErrorBody"
            }
          }
        }
      },
      "NotepadNotFound": {
        "description": "No such record the caller can see. Deliberately indistinguishable from \"exists but you may not see it\" — existence is not leaked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotepadErrorBody"
            }
          }
        }
      },
      "NotepadError": {
        "description": "Refused, with a machine-readable code.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotepadErrorBody"
            }
          }
        }
      },
      "NotepadValidationError": {
        "description": "Validation failure, one entry per rejected field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotepadValidationErrorBody"
            }
          }
        }
      },
      "AppointmentSchedulerUnauthenticated": {
        "description": "No valid token or session (`{ \"error\": { \"code\", \"message\" } }`), or a\nrequest that authenticated without a person — \"Appointment Scheduler\nrequires a signed-in user\" (`{ \"success\": false, \"message\" }`).\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              ]
            }
          }
        }
      },
      "AppointmentSchedulerForbidden": {
        "description": "- `insufficient_permissions` — the token lacks the scope this endpoint names (`{ \"error\": { \"code\", \"message\" } }`).\n- \"Appointment Scheduler is not enabled for this business\" (`{ \"success\": false, \"message\" }`).\n- \"You don't have access to Appointment Scheduler\" — Access Management does not admit the caller (`{ \"success\": false, \"message\" }`).\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              ]
            }
          }
        }
      },
      "AppointmentSchedulerCannotChange": {
        "description": "Every refusal `AppointmentSchedulerForbidden` lists, and also \"Only\nthis appointment's host, its booking page's owner, or an Appointment\nScheduler admin can change it\" — the caller can read the appointment\n(a collective meeting's co-host) but not change it.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/AppointmentSchedulerRefusal"
                }
              ]
            }
          }
        }
      },
      "AskAiPaymentRequired": {
        "description": "`upgrade_required`: the organization's plan does not include Ask AI\n(`error.details` carries `feature`, `required_tier`,\n`required_tier_name`, `current_tier`, `current_tier_name`). Or\n`api_credits_exhausted`: a call with a created API token after the\nmonth's API allowance is used up.\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"
              ]
            }
          }
        }
      },
      "AskAiReadRefused": {
        "description": "`app_disabled`: Ask AI is not enabled for this business or not open to\nthis person. `extension_disabled`: a Chrome extension request while the\norganization has the extension turned off. `insufficient_permissions`:\nthe API token lacks `read:chat` (`read:own_chat`, `read:all` and `admin`\nalso pass; signed-in sessions are not checked).\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"
              ]
            }
          }
        }
      },
      "AskAiWriteRefused": {
        "description": "`app_disabled`: Ask AI is not enabled for this business or not open to\nthis person. `extension_disabled`: a Chrome extension request while the\norganization has the extension turned off. `insufficient_permissions`:\nthe API token lacks `write:chat` (`write:own_chat` and `admin` also\npass; signed-in sessions are not checked).\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"
              ]
            }
          }
        }
      },
      "AskAiEntitlementUnavailable": {
        "description": "`entitlement_unavailable`: the check of the organization's plan failed — try again in a minute.",
        "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"
              ]
            }
          }
        }
      },
      "BulkOperationQueued": {
        "description": "The operation was saved and queued.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "bulk_operation",
                "job_id",
                "status_url"
              ],
              "properties": {
                "bulk_operation": {
                  "type": "object",
                  "description": "The saved operation record, `metadata` (the submitted items and options) included."
                },
                "job_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "status_url": {
                  "type": "string",
                  "example": "/api/v1/bulk_operations/status/0b6f3c3e-8d5a-4a8e-9f0e-2c1d7a9e4b11"
                }
              }
            }
          }
        }
      },
      "BulkOperationForbidden": {
        "description": "`insufficient_permissions` in the standard error envelope (the token lacks the data type's write scope, or `destructive:scheduling` for a final act), or `{ \"error\": \"…\" }`: not an admin or manager, Shifts & Scheduling not enabled, an administrator needed for demand signals and forecasts, or items outside the caller's team or locations (\"No items were processed\").",
        "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"
                  ]
                },
                {
                  "$ref": "#/components/schemas/BulkOperationRefusal"
                }
              ]
            }
          }
        }
      },
      "BulkOperationRefused": {
        "description": "`{ \"error\": \"No items provided\" }`, `{ \"errors\": [...] }` when the operation fails validation (an unknown `data_type` or `operation_type`), or `{ \"error\": \"No processor is available for '…'\" }`.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "CommsHubPublicationsForbidden": {
        "description": "Every refusal is the `{\"error\": {\"code\", \"message\"}}` envelope:\n- `insufficient_permissions` — the token lacks `read:broadcasts` (\"Required scope: read:broadcasts\").\n- `forbidden` — \"Comms Hub is not enabled for this business.\" (the Communications app is off), \"You don't have access to Comms Hub.\" (the app is not published to the caller), \"No business context for this request.\", or \"Publications is disabled for this business.\" (an admin turned the newsletters module, `enable_publications`, off).\n- `work_context_restricted` — an admin's Communications access policy refuses the caller's work context; the body also carries `code` and `error_detail` at the top level.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CommsHubAudiencesForbidden": {
        "description": "Every refusal is the `{\"error\": {\"code\", \"message\"}}` envelope:\n- `insufficient_permissions` — the token lacks `read:broadcasts` (\"Required scope: read:broadcasts\").\n- `forbidden` — \"You don't have permission to view audiences.\" (the caller may not publish Communications), \"Audiences is disabled for this business.\" (an admin turned the Audiences module, `enable_audiences`, off), \"Comms Hub is not enabled for this business.\", \"You don't have access to Comms Hub.\" or \"No business context for this request.\".\n- `work_context_restricted` — an admin's Communications access policy refuses the caller's work context; the body also carries `code` and `error_detail` at the top level.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ExportFilesQueued": {
        "description": "Saved and queued; poll `status_url` until `status` is `completed`",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "export_file",
                "job_id",
                "status_url"
              ],
              "properties": {
                "export_file": {
                  "$ref": "#/components/schemas/ExportFilesExportFile"
                },
                "job_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "status_url": {
                  "type": "string",
                  "example": "/api/v1/export_files/status/0b6f1c1e-3c55-4f0b-9d0e-6a3c1f2f9a10"
                }
              }
            }
          }
        }
      },
      "ExportFilesNotQueued": {
        "description": "Nothing was queued. Either the export was not saved — `{\"errors\":\n[\"Export type is not included in the list\"]}`, \"Format is not\nincluded in the list\", … — or no export job exists for its type: the\nrow is saved as `failed` and returned beside `{\"error\": {\"code\":\n\"validation_failed\", \"message\": \"No export is available for '…'\"}}`\nas `export_file`. Or the request sent a non-empty `filters` or\n`options`, refused before anything is saved: `{\"error\": {\"code\":\n\"unsupported_parameter\", \"message\": \"Exports take no filters: the\nfile holds every row of its type. Send only the format, and filter\nthe file after downloading it.\"}}`.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ExportFilesValidationMessages"
                },
                {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "export_file": {
                          "$ref": "#/components/schemas/ExportFilesExportFile"
                        }
                      }
                    }
                  ]
                }
              ]
            }
          }
        }
      },
      "ExportFilesNotScheduled": {
        "description": "`service_unavailable` — the job could not be queued (\"Could not\nschedule the export. Please try again.\"). The row is saved as\n`failed` and returned beside the error as `export_file`.\n",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "type": "object",
                  "properties": {
                    "export_file": {
                      "$ref": "#/components/schemas/ExportFilesExportFile"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "IntelligenceBusinessContextRequired": {
        "description": "`invalid_request` — \"Business context required\": the request resolved no business.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IntelligenceRateLimited": {
        "description": "`rate_limited` — \"Rate limit exceeded. Please try again later.\": the caller has made 100 `smart_defaults` / `contextual_suggestions` calls without an hour's gap. `retry_after` is in seconds.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "type": "object",
                  "properties": {
                    "retry_after": {
                      "type": "integer",
                      "example": 3600
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "MangoSignalForbidden": {
        "description": "`insufficient_permissions` (the token lacks the scope), `mango_signal_disabled`, or the endpoint's role rule (`manager_access_required`, `admin_access_required`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/MangoSignalRefusal"
            }
          }
        }
      },
      "MealComplianceWaiverActionDone": {
        "description": "Done: the sentence the web page flashes, its `tone` (`warning` when the write landed but the employee could not be told — tell them directly), and the waiver page as it now stands — the same body as `GET /meal_compliance/waivers/{id}`, so the client redraws without a second request.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "tone": {
                      "type": "string",
                      "enum": [
                        "success",
                        "warning"
                      ]
                    }
                  }
                },
                {
                  "$ref": "#/components/schemas/MealComplianceWaiverDetails"
                }
              ]
            },
            "example": {
              "message": "Waiver has been invalidated and Omar Haddad has been notified.",
              "tone": "success",
              "waiver": {
                "id": 904,
                "status": "invalidated",
                "status_label": "Invalidated",
                "tone": "danger"
              },
              "permissions": {
                "can_invalidate": false,
                "can_start_new_waiver": true
              },
              "actions": []
            }
          }
        }
      },
      "MealComplianceWaiverActionForbidden": {
        "description": "The token lacks the write scope (`insufficient_permissions`), the app is not open to the caller, the waiver belongs to someone the caller may not see, or the caller may see it but does not manage it — an employee on their own waiver, a line manager on their own, any own-tier token (`forbidden`)."
      },
      "MuscleMemoryReadForbidden": {
        "description": "- `insufficient_permissions` — the token lacks `read:muscle_memory` (\"Required scope: read:muscle_memory\").\n- `app_disabled` — Muscle Memory is not enabled for the business (\"Muscle Memory is not enabled for this business.\").\n- `forbidden` — Muscle Memory is not published to the caller (\"You don't have access to Muscle Memory.\"), or, on the list, `status=recheck` from a caller who does not hold the curate capability (\"You don't have permission to curate business lessons for this business.\").\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MuscleMemoryCurateForbidden": {
        "description": "- `insufficient_permissions` — the token lacks the scope this call needs (\"Required scope: write:muscle_memory\", or \"Required scope: destructive:muscle_memory\" for DELETE).\n- `app_disabled` — Muscle Memory is not enabled for the business (\"Muscle Memory is not enabled for this business.\").\n- `forbidden` — Muscle Memory is not published to the caller (\"You don't have access to Muscle Memory.\"), or the caller does not hold the curate capability (\"You don't have permission to curate business lessons for this business.\"; for `POST /muscle-memory/lessons` the message adds that a lesson can be suggested from the Muscle Memory page instead).\n\nThe capability is checked before the id is looked up, so a caller who may not curate gets `403` whatever id they send.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MuscleMemoryLessonNotFound": {
        "description": "`not_found` — no such lesson in this business (\"Lesson not found.\" or \"Memory not found.\"), or it is a sample lesson. For `GET`, a forgotten lesson is not found either.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MuscleMemoryInternalError": {
        "description": "`internal_server_error` — \"Something went wrong. Please try again.\" The details are logged, never returned. Nothing was changed by a failed call except where the response says otherwise.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NewsFeedFeedNotFound": {
        "description": "`not_found` \"Feed not found\" — no such post in the business, or a draft or scheduled post the caller is not preparing.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NewsFeedMediaNotFound": {
        "description": "`not_found` — \"Feed not found\" (as above) or \"Media not found\" (no live attachment with this id on the post).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NewsFeedMediaReadForbidden": {
        "description": "- `insufficient_permissions` — the token lacks `read:news_feed`.\n- `not_authorized` — \"You do not have access to this feed\": the caller is not in the post's audience and may not view any post.\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NewsFeedMediaWriteForbidden": {
        "description": "- `insufficient_permissions` — the token lacks `write:news_feed`.\n- `feed_not_editable` — the caller may not change this post's attachments. The message says why: \"You can't change the attachments on someone else's post.\", \"Attachments on this post can no longer be changed.\" (content editing is off, or someone acknowledged it), or \"Only <name> and the person writing it for them can change this post's attachments before it goes out.\"\n- `app_disabled` / `app_unavailable` / `work_context_restricted` / `forbidden` — the Communications app door (see `CommunicationsUnavailable`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NewsFeedMediaRefused": {
        "description": "The upload was refused (`{\"error\": {\"code\", \"message\"}}`):\n`invalid` (a missing parameter or an unknown `media_type`),\n`media_uploads_disabled`, `gif_uploads_disabled`,\n`attachment_cap_reached` (the post already has 10 attachments),\n`file_too_large`, `blocked_mime`, `mime_not_allowed`,\n`blocked_extension`, `alt_text_required`, `direct_upload_unsupported`\n(the JSON leg on a deployment that does not store files in S3),\n`storage_unavailable`; for a comment's file `invalid_parent_type`,\n`comment_not_found`, `comment_feed_mismatch`, `comment_deleted`,\n`not_comment_author`, `edit_window_expired`,\n`comment_attachment_cap_reached`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RecorderRecordingNotFound": {
        "description": "`recording_not_found` — no such recording visible to the caller.",
        "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"
              ]
            }
          }
        }
      },
      "RecorderIdempotencyConflict": {
        "description": "`idempotency_conflict` — this Idempotency-Key was used for a different request; or `idempotency_in_progress` — a request with this key is still running (retry after the `Retry-After` seconds to get its answer).",
        "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"
              ]
            }
          }
        }
      },
      "RecorderMediaTooLarge": {
        "description": "`file_too_large` — over the workspace's upload cap; `error.details` carries `max_bytes` and `allowed_types`.",
        "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"
              ]
            }
          }
        }
      },
      "RecorderMediaTypeUnsupported": {
        "description": "`unsupported_media_type` — not a WebM, MP4 or QuickTime video or a PNG or JPEG image (by its declared type, or by its bytes); `error.details` carries `max_bytes` and `allowed_types`.",
        "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"
              ]
            }
          }
        }
      },
      "RecorderProcessingStarted": {
        "description": "The media is attached and processing has started.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "recording",
                "processing"
              ],
              "properties": {
                "recording": {
                  "$ref": "#/components/schemas/RecorderRecording"
                },
                "processing": {
                  "type": "object",
                  "properties": {
                    "started": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        }
      },
      "SitesApprovalRevisionMissing": {
        "description": "`parameter_missing` — `review_revision` was not sent.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "code": "parameter_missing",
                "message": "Send review_revision — the row's review.revision — with a decision.",
                "details": {
                  "parameter": "review_revision"
                }
              }
            }
          }
        }
      },
      "SitesApprovalDecisionForbidden": {
        "description": "`own_request` — the caller asked for this approval, so someone else decides it (they can withdraw it instead); `on_hold` — the page's site is in the Trash; `not_approver` — only the approver it was sent to, or an admin, can decide it; `approvals_disabled` — page approvals are turned off for the company; `insufficient_permissions` — the token lacks `write:sites`; `app_disabled` / `app_access_denied` — the Sites app is off for the company or not open to the caller.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "code": "own_request",
                "message": "You asked for this approval, so someone else has to decide it. You can withdraw the request instead."
              }
            }
          }
        }
      },
      "SitesApprovalDecisionConflict": {
        "description": "`not_pending` — the page isn't waiting for approval (already decided or withdrawn); `review_changed` — the request changed after the caller loaded it. Reload the Approvals list and look again.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "code": "review_changed",
                "message": "Could not approve: This approval request changed. Reload the page and check the latest draft before deciding."
              }
            }
          }
        }
      },
      "SitesCareTransferPending": {
        "description": "`transfer_pending` — the page or its site is still being imported; the message says where the import stands.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "code": "transfer_pending",
                "message": "This import is still being prepared. Try again once it finishes."
              }
            }
          }
        }
      },
      "SitesCareRefused": {
        "description": "`access_denied` — the caller can neither open nor edit the page, so nothing else about it is said; `not_allowed` — only the site's owners and editors and admins can do this; `site_trashed` (extend) — the site is in Trash; `content_sync_disabled` (source refresh); `insufficient_permissions` — the token lacks the scope.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "code": "not_allowed",
                "message": "Only this site's owners and editors can do this."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ServiceDeskProblemUser": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ServiceDeskProblemIncident": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "priority": {
            "type": "string"
          },
          "requester": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceDeskProblemChangeRequest": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "change_number": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "ServiceDeskProblemRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "problem_number": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "ServiceDeskProblemInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 255,
            "description": "Required on create"
          },
          "description": {
            "type": "string"
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "network",
              "hardware",
              "software",
              "security",
              "process",
              "other"
            ]
          },
          "assignee_id": {
            "type": "integer",
            "nullable": true
          },
          "service_department_id": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "ServiceDeskProblemSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "problem_number": {
            "type": "string",
            "example": "PRB-2026-0001"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "priority": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "known_error": {
            "type": "boolean"
          },
          "linked_incident_count": {
            "type": "integer",
            "description": "Incidents this caller may see — grievances they cannot view are not counted"
          },
          "duplicate_of_id": {
            "type": "integer",
            "nullable": true,
            "description": "Set when the problem was closed as a duplicate of another"
          },
          "assignee": {
            "$ref": "#/components/schemas/ServiceDeskProblemUser"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceDeskProblemDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ServiceDeskProblemSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "identified_by": {
                "$ref": "#/components/schemas/ServiceDeskProblemUser"
              },
              "service_department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "root_cause": {
                "type": "string",
                "nullable": true
              },
              "root_cause_method": {
                "type": "string",
                "nullable": true
              },
              "root_cause_found_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "workaround": {
                "type": "string",
                "nullable": true
              },
              "known_error_description": {
                "type": "string",
                "nullable": true
              },
              "known_error_since": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "resolution": {
                "type": "string",
                "nullable": true
              },
              "resolved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "closure_notes": {
                "type": "string",
                "nullable": true
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "linked_incident_count": {
                "type": "integer",
                "description": "Incidents this caller may see"
              },
              "linked_incidents": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskProblemIncident"
                }
              },
              "configuration_items": {
                "type": "array",
                "nullable": true,
                "description": "Linked configuration items; null while the tenant has the CMDB module turned off (CI links are refused then).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "ci_number": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "ci_type": {
                      "type": "string"
                    },
                    "criticality": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              },
              "investigation_entries": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "entry_type": {
                      "type": "string",
                      "nullable": true
                    },
                    "content": {
                      "type": "string"
                    },
                    "user": {
                      "$ref": "#/components/schemas/ServiceDeskProblemUser"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "timeline": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "action": {
                      "type": "string"
                    },
                    "from_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "to_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "details": {
                      "type": "string",
                      "nullable": true
                    },
                    "user": {
                      "$ref": "#/components/schemas/ServiceDeskProblemUser"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "fix_change_request": {
                "$ref": "#/components/schemas/ServiceDeskProblemChangeRequest"
              },
              "duplicate_of": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ServiceDeskProblemRef"
                  }
                ],
                "nullable": true,
                "description": "The problem this one was closed as a duplicate of"
              },
              "duplicates": {
                "type": "array",
                "description": "Problems closed as duplicates of this one",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskProblemRef"
                }
              },
              "permissions": {
                "type": "object",
                "properties": {
                  "can_edit": {
                    "type": "boolean"
                  },
                  "available_events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "can_publish_known_error": {
                    "type": "boolean"
                  },
                  "can_unpublish_known_error": {
                    "type": "boolean"
                  },
                  "can_create_change_request": {
                    "type": "boolean"
                  },
                  "can_close_as_duplicate": {
                    "type": "boolean",
                    "description": "Close as a duplicate is offered (needs destructive:service_desk)"
                  },
                  "can_reopen": {
                    "type": "boolean",
                    "description": "Reopen is offered — a closed problem",
                    "a Service Desk admin": null
                  }
                }
              }
            }
          }
        ]
      },
      "ServiceDeskProblemEnvelope": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "problem": {
            "$ref": "#/components/schemas/ServiceDeskProblemDetail"
          }
        }
      },
      "ChangeRequestPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ChangeRequestInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "change_type": {
            "type": "string",
            "enum": [
              "standard",
              "normal",
              "emergency"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "implementation_plan": {
            "type": "string"
          },
          "rollback_plan": {
            "type": "string"
          },
          "test_plan": {
            "type": "string"
          },
          "communication_plan": {
            "type": "string"
          },
          "reason_for_change": {
            "type": "string"
          },
          "scheduled_start_at": {
            "type": "string",
            "format": "date-time"
          },
          "scheduled_end_at": {
            "type": "string",
            "format": "date-time"
          },
          "emergency_justification": {
            "type": "string"
          },
          "service_department_id": {
            "type": "integer"
          },
          "support_team_id": {
            "type": "integer"
          },
          "related_incident_id": {
            "type": "integer"
          },
          "assignee_id": {
            "type": "integer"
          },
          "cab_id": {
            "type": "integer"
          }
        }
      },
      "ChangeRequestSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "change_number": {
            "type": "string",
            "example": "RFC-2026-0001"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "pending_review",
              "approved",
              "scheduled",
              "implementing",
              "implemented",
              "closed",
              "failed",
              "cancelled"
            ]
          },
          "status_label": {
            "type": "string",
            "example": "Pending Review",
            "description": "The label the web badge shows: `submitted` reads \"Pending Review\" (\"Returned\" once a CAB rejected it), `pending_review` reads \"In CAB Review\""
          },
          "change_type": {
            "type": "string",
            "enum": [
              "standard",
              "normal",
              "emergency"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "risk_level": {
            "type": "string",
            "nullable": true,
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "risk_score": {
            "type": "integer",
            "nullable": true
          },
          "requester": {
            "$ref": "#/components/schemas/ChangeRequestPerson"
          },
          "assignee": {
            "$ref": "#/components/schemas/ChangeRequestPerson"
          },
          "cab": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "scheduled_start_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "scheduled_end_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "review_sla_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ChangeRequestDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ChangeRequestSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "reason_for_change": {
                "type": "string",
                "nullable": true
              },
              "implementation_plan": {
                "type": "string",
                "nullable": true
              },
              "rollback_plan": {
                "type": "string",
                "nullable": true
              },
              "test_plan": {
                "type": "string",
                "nullable": true
              },
              "communication_plan": {
                "type": "string",
                "nullable": true
              },
              "emergency_justification": {
                "type": "string",
                "nullable": true
              },
              "rejection_reason": {
                "type": "string",
                "nullable": true
              },
              "closure_code": {
                "type": "string",
                "nullable": true
              },
              "closure_notes": {
                "type": "string",
                "nullable": true
              },
              "lessons_learned": {
                "type": "string",
                "nullable": true
              },
              "approval_rule": {
                "type": "string",
                "nullable": true
              },
              "emergency_bypass": {
                "type": "boolean"
              },
              "review_sla_breached": {
                "type": "boolean"
              },
              "risk_factors": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "service_department_id": {
                "type": "integer",
                "nullable": true
              },
              "support_team_id": {
                "type": "integer",
                "nullable": true
              },
              "related_incident_id": {
                "type": "integer",
                "nullable": true
              },
              "actual_start_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "actual_end_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "approved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "rejected_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "approval_summary": {
                "type": "object",
                "properties": {
                  "total": {
                    "type": "integer"
                  },
                  "decided": {
                    "type": "integer"
                  },
                  "pending": {
                    "type": "integer"
                  },
                  "approved": {
                    "type": "integer"
                  },
                  "rejected": {
                    "type": "integer"
                  },
                  "deferred": {
                    "type": "integer"
                  }
                }
              },
              "approvals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "approver": {
                      "$ref": "#/components/schemas/ChangeRequestPerson"
                    },
                    "decision": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "approved",
                        "rejected",
                        "deferred",
                        "more_info_requested"
                      ]
                    },
                    "comments": {
                      "type": "string",
                      "nullable": true
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              },
              "configuration_items": {
                "type": "array",
                "nullable": true,
                "description": "Linked configuration items; null while the tenant has the CMDB module turned off (CI links are refused then).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "ci_number": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "ci_type": {
                      "type": "string"
                    },
                    "criticality": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              },
              "attachments_count": {
                "type": "integer"
              },
              "timeline": {
                "type": "array",
                "description": "Newest 200 entries, oldest first",
                "items": {
                  "type": "object",
                  "properties": {
                    "action": {
                      "type": "string",
                      "nullable": true
                    },
                    "from_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "to_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "from_status_label": {
                      "type": "string",
                      "nullable": true
                    },
                    "to_status_label": {
                      "type": "string",
                      "nullable": true
                    },
                    "details": {
                      "type": "string",
                      "nullable": true
                    },
                    "user": {
                      "$ref": "#/components/schemas/ChangeRequestPerson"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "timeline_truncated": {
                "type": "boolean"
              },
              "permissions": {
                "type": "object",
                "description": "What the caller may do right now",
                "properties": {
                  "can_edit": {
                    "type": "boolean"
                  },
                  "can_make_emergency": {
                    "type": "boolean",
                    "description": "May set change_type to emergency in an edit — the requester or an admin; other staff editing a colleague's draft may not (an emergency change is approved the moment it is submitted). A PATCH that tries answers 403."
                  },
                  "can_submit": {
                    "type": "boolean"
                  },
                  "can_resubmit": {
                    "type": "boolean"
                  },
                  "can_review": {
                    "type": "boolean",
                    "description": "May approve or reject (the review). Reject also needs destructive:service_desk — read can_reject for it."
                  },
                  "can_reject": {
                    "type": "boolean",
                    "description": "May reject the change: can_review, and a token holding destructive:service_desk (#7077)."
                  },
                  "can_vote": {
                    "type": "boolean"
                  },
                  "can_route_to_cab": {
                    "type": "boolean"
                  },
                  "can_assign": {
                    "type": "boolean"
                  },
                  "can_schedule": {
                    "type": "boolean"
                  },
                  "can_start_implementation": {
                    "type": "boolean"
                  },
                  "can_complete_implementation": {
                    "type": "boolean"
                  },
                  "can_fail_implementation": {
                    "type": "boolean"
                  },
                  "can_close": {
                    "type": "boolean"
                  },
                  "can_cancel": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        ]
      },
      "AccessRequestPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        }
      },
      "AccessRequestSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "executed",
              "cancelled",
              "expired"
            ]
          },
          "target_type": {
            "type": "string",
            "example": "Location"
          },
          "target_type_label": {
            "type": "string",
            "example": "Location",
            "description": "Display label — Workspace reads Project, AssetPro::Category reads Equipment"
          },
          "target_id": {
            "type": "integer"
          },
          "target_name": {
            "type": "string"
          },
          "requested_role": {
            "type": "string",
            "nullable": true
          },
          "requester": {
            "$ref": "#/components/schemas/AccessRequestPerson"
          },
          "approver": {
            "$ref": "#/components/schemas/AccessRequestPerson"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "can_approve": {
            "type": "boolean",
            "description": "The caller can approve or reject this request now"
          }
        }
      },
      "AccessRequestDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AccessRequestSummary"
          },
          {
            "type": "object",
            "properties": {
              "justification": {
                "type": "string"
              },
              "approval_notes": {
                "type": "string",
                "nullable": true
              },
              "rejection_reason": {
                "type": "string",
                "nullable": true
              },
              "executed_by": {
                "$ref": "#/components/schemas/AccessRequestPerson"
              },
              "help_request_id": {
                "type": "integer",
                "nullable": true,
                "description": "The linked Service Desk ticket"
              },
              "approved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "rejected_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "executed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "cancelled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "permissions": {
                "type": "object",
                "description": "The actions the web detail page offers this caller",
                "properties": {
                  "can_approve": {
                    "type": "boolean"
                  },
                  "can_reject": {
                    "type": "boolean"
                  },
                  "can_cancel": {
                    "type": "boolean"
                  },
                  "can_resubmit": {
                    "type": "boolean"
                  }
                }
              },
              "audit_trail": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "capability": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "AccessRequestWriteResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "message": {
            "type": "string"
          },
          "access_request": {
            "$ref": "#/components/schemas/AccessRequestDetail"
          }
        }
      },
      "AccessRequestError": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "object",
            "description": "`code` is the root `code` when the refusal carries one, else the code for the response status (400 `invalid_request`, 403 `forbidden`, 404 `not_found`, 422 `validation_failed`); `message` is the sentence.",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "code": {
            "type": "string",
            "nullable": true,
            "description": "The refusal's own code, when it has one (also `error.code`)."
          }
        }
      },
      "ConfigurationItemRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "ci_number": {
            "type": "string",
            "example": "CI-00012"
          },
          "ci_type": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "criticality": {
            "type": "string"
          }
        }
      },
      "CmdbPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ConfigurationItemSummary": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ConfigurationItemRef"
          },
          {
            "type": "object",
            "properties": {
              "display_name": {
                "type": "string",
                "example": "Web Server 01 (CI-00012)"
              },
              "environment": {
                "type": "string",
                "nullable": true
              },
              "environment_label": {
                "type": "string",
                "example": "Production"
              },
              "retired": {
                "type": "boolean"
              },
              "owner": {
                "$ref": "#/components/schemas/CmdbPerson"
              },
              "service_department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "ConfigurationItemDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ConfigurationItemSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "nullable": true
              },
              "location": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "asset": {
                "type": "object",
                "nullable": true,
                "description": "The Asset Pro asset this CI is",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "asset_number": {
                    "type": "string"
                  }
                }
              },
              "attributes_data": {
                "type": "object",
                "additionalProperties": true,
                "description": "Custom attributes (key → value)"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "ConfigurationItemRelationship": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "relationship_type": {
            "type": "string",
            "enum": [
              "depends_on",
              "runs_on",
              "part_of",
              "connects_to",
              "uses"
            ]
          },
          "label": {
            "type": "string",
            "description": "Worded from this CI's side, e.g. \"Depends on\" upstream, \"Depended on by\" downstream"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "configuration_item": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ConfigurationItemRef"
              },
              {
                "type": "object",
                "properties": {
                  "display_name": {
                    "type": "string"
                  },
                  "owner": {
                    "$ref": "#/components/schemas/CmdbPerson"
                  }
                }
              }
            ]
          }
        }
      },
      "ConfigurationItemInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 255
          },
          "description": {
            "type": "string"
          },
          "ci_type": {
            "type": "string",
            "enum": [
              "service",
              "server",
              "application",
              "database",
              "network",
              "storage",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "planned",
              "retired"
            ]
          },
          "environment": {
            "type": "string",
            "enum": [
              "production",
              "staging",
              "development",
              "dr"
            ]
          },
          "criticality": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "owner_id": {
            "type": "integer"
          },
          "service_department_id": {
            "type": "integer"
          },
          "location_id": {
            "type": "integer"
          },
          "asset_pro_asset_id": {
            "type": "integer"
          },
          "attributes_data": {
            "type": "object",
            "additionalProperties": true,
            "description": "A JSON object under 10KB (a JSON string of one is accepted too). `{}` clears."
          }
        }
      },
      "ConfigurationItemOptions": {
        "type": "object",
        "properties": {
          "ci_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "statuses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "environments": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "criticalities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "relationship_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "relationship_directions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string",
                  "enum": [
                    "child",
                    "parent"
                  ]
                },
                "label": {
                  "type": "string",
                  "example": "Child of this CI"
                }
              }
            }
          }
        }
      },
      "CmdbValidationError": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "object",
            "description": "`code` is the code for the response status (400 `invalid_request`, 403 `forbidden`, 404 `not_found`, 409 `conflict`, 422 `validation_failed`, 503 `service_unavailable`); `message` is the sentence.",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ServiceDeskGrievancePerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ServiceDeskGrievanceStageRef": {
        "type": "object",
        "nullable": true,
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ServiceDeskGrievanceInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Required on create"
          },
          "description": {
            "type": "string",
            "description": "Required on create"
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "Answers keyed by question field_name"
          },
          "assignee_id": {
            "type": "integer",
            "nullable": true,
            "description": "Handlers only"
          },
          "support_team_id": {
            "type": "integer",
            "nullable": true,
            "description": "Handlers only"
          },
          "service_department_id": {
            "type": "integer",
            "nullable": true,
            "description": "Handlers only"
          },
          "anonymous": {
            "type": "boolean",
            "description": "Always refused when true"
          }
        }
      },
      "ServiceDeskGrievanceComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "content": {
            "type": "string"
          },
          "author": {
            "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
          },
          "internal": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ServiceDeskGrievanceSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "stage": {
            "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
          },
          "stage_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stage_overdue": {
            "type": "boolean"
          },
          "grievant_name": {
            "type": "string"
          },
          "grievance_type": {
            "type": "string",
            "nullable": true
          },
          "filing_date": {
            "type": "string",
            "nullable": true
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "priority": {
            "type": "string",
            "description": "Absent while the tenant hides priority from this caller"
          },
          "assignee": {
            "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
          }
        }
      },
      "ServiceDeskGrievanceDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ServiceDeskGrievanceSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string"
              },
              "requester_name": {
                "type": "string",
                "nullable": true
              },
              "support_team": {
                "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
              },
              "stages": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "current",
                        "upcoming"
                      ]
                    }
                  }
                }
              },
              "custom_fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "value": {},
                    "display_value": {
                      "type": "string"
                    }
                  }
                }
              },
              "stage_history": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "content": {
                      "type": "string",
                      "description": "Staff notes included for handlers only"
                    },
                    "from_stage": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
                    },
                    "to_stage": {
                      "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
                    },
                    "author": {
                      "$ref": "#/components/schemas/ServiceDeskGrievancePerson"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "comments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskGrievanceComment"
                }
              },
              "comments_truncated": {
                "type": "boolean",
                "description": "Only the newest 200 comments are included"
              },
              "attachments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "content_type": {
                      "type": "string"
                    },
                    "byte_size": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string",
                      "description": "Download link",
                      "expires after an hour": null
                    }
                  }
                }
              },
              "allowed_transitions": {
                "type": "array",
                "description": "Handlers only",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
                }
              },
              "revert_targets": {
                "type": "array",
                "description": "Admins only",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskGrievanceStageRef"
                }
              },
              "permissions": {
                "type": "object",
                "properties": {
                  "can_edit": {
                    "type": "boolean"
                  },
                  "can_advance_stage": {
                    "type": "boolean"
                  },
                  "can_revert_stage": {
                    "type": "boolean"
                  },
                  "can_comment": {
                    "type": "boolean"
                  },
                  "can_add_internal_note": {
                    "type": "boolean"
                  },
                  "can_upload_document": {
                    "type": "boolean"
                  },
                  "can_remove_document": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        ]
      },
      "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",
            "description": "Omitted when the tenant turned on \"Hide priority from requester\" and the caller is the requester, or is neither Service Desk staff nor a \"view all tickets\" holder."
          },
          "priority_color_class": {
            "type": "string",
            "description": "Bootstrap badge class mapped from priority. Omitted whenever `priority` is.",
            "example": "bg-danger"
          },
          "escalated": {
            "type": "boolean",
            "description": "Escalated and not since de-escalated — stays true after the ticket is resolved or closed (see escalation_active)"
          },
          "escalation_active": {
            "type": "boolean",
            "description": "Escalated and not yet finished — what the web badges as \"Escalated\""
          },
          "request_type": {
            "type": "string"
          },
          "requester": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true,
                "description": "Null on an anonymous ticket (name reads \"Anonymous\")"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "description": "Null on an anonymous ticket, or a requester with no email address"
              }
            }
          },
          "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",
            "nullable": true,
            "description": "True when the SLA deadline (sla_due_at) has passed and the ticket is still open. Null for a caller who is not shown internal fields (e.g. the requester)."
          },
          "sla_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "SLA deadline computed from priority and assignment time. An internal field — shown to Service Desk staff, managers, the assignee and the approver; null for everyone else."
          },
          "response_expected_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the service desk expects to resolve the ticket (the priority's time-to-resolve target) — the \"Resolution expected by\" time the web shows a requester instead of the SLA deadline. Set only for a caller who is not shown sla_due_at, only while the ticket is open and not waiting on approval, and only when the desk has SLA on."
          },
          "rejected_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a rejected request was turned down; null unless the status is rejected."
          },
          "rejection_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why a rejected request was turned down — what the ticket page's \"Request Rejected\" banner shows."
          }
        }
      },
      "SupportTicketDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SupportTicketSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string"
              },
              "escalated_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "escalation_reason": {
                "type": "string",
                "nullable": true,
                "description": "Service Desk staff only; null for everyone else"
              },
              "approved_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "escalated_to": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "rejected_by": {
                "type": "object",
                "nullable": true,
                "description": "Who turned a rejected request down; null unless the status is rejected.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "location": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "urgent": {
                "type": "boolean",
                "nullable": true
              },
              "satisfaction_rating": {
                "type": "integer",
                "nullable": true,
                "minimum": 1,
                "maximum": 5
              },
              "satisfaction_feedback": {
                "type": "string",
                "nullable": true
              },
              "comments_truncated": {
                "type": "boolean",
                "description": "True when `comments` holds only the newest 50 of `comments_count`; GET /:id/comments has the whole thread"
              },
              "resolution_notes": {
                "type": "string",
                "nullable": true
              },
              "resolved_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "anonymous": {
                "type": "boolean",
                "description": "Filed anonymously — `requester.id` / `email` are then null and `requester.name` is \"Anonymous\"."
              },
              "urgency_reason": {
                "type": "string",
                "nullable": true
              },
              "custom_fields": {
                "type": "array",
                "description": "Answered custom fields in the admin's layout order, blank ones omitted. Internal-only fields are included for Service Desk staff only.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "The field's `field_name`"
                    },
                    "label": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string",
                      "description": "Display text (\"Yes\"/\"No\" for a checkbox, a formatted date, a joined multiselect)"
                    }
                  }
                }
              },
              "comments_count": {
                "type": "integer"
              },
              "comments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TicketComment"
                }
              },
              "permissions": {
                "$ref": "#/components/schemas/TicketPermissions"
              },
              "source": {
                "type": "string",
                "nullable": true,
                "description": "Where the ticket came from (web, mobile, email, api, …)"
              },
              "service_department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "support_team": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "share_url": {
                "type": "string",
                "description": "Link to the ticket on the web, for sharing"
              },
              "followers": {
                "type": "array",
                "description": "People on the ticket. Empty when the caller may not see them; a requester sees only people added by hand.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "avatar_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "follow_type": {
                      "type": "string"
                    }
                  }
                }
              },
              "time_worked_minutes": {
                "type": "integer",
                "nullable": true,
                "description": "Total time logged on the ticket. Service Desk staff only; null for everyone else."
              },
              "estimated_cost": {
                "type": "number",
                "nullable": true,
                "description": "The web ticket page's Work & Cost \"Estimated\" figure. An internal field — shown to Service Desk staff, managers, the assignee and the approver (the same people as sla_due_at); null for everyone else, and null when the ticket has none."
              },
              "actual_cost": {
                "type": "number",
                "nullable": true,
                "description": "The Work & Cost \"Actual\" figure, as an agent entered it — parts issued from inventory are not added to it (see `parts.total_cost`). Same visibility as estimated_cost; null when the ticket has none."
              },
              "over_budget": {
                "type": "boolean",
                "nullable": true,
                "description": "True when both figures are set and actual_cost is above estimated_cost — the web page draws Actual in red with an over-budget warning. Null for a caller who is not shown the cost figures."
              },
              "parts": {
                "type": "object",
                "nullable": true,
                "description": "The web ticket page's Work & Cost › Parts & materials card. An object (its lists possibly empty) for Service Desk staff with the Procurement app on and open to them — the rule behind `permissions.can_manage_parts` — and null for everyone else, so a requester never sees what the job cost internally. Show the card whenever this is not null; offer its Request parts action when `permissions.can_manage_parts` is true (false for a token without write:service_desk).",
                "properties": {
                  "total_cost": {
                    "type": "number",
                    "description": "Net cost of every part issued to this job (returns subtract), across all movements — not only the `items` window."
                  },
                  "truncated": {
                    "type": "boolean",
                    "description": "True when `items` holds only the 20 most recent movements."
                  },
                  "items": {
                    "type": "array",
                    "description": "Parts issued to or returned from this job, newest first.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "item": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "quantity": {
                          "type": "integer"
                        },
                        "direction": {
                          "type": "string",
                          "enum": [
                            "issued",
                            "returned"
                          ]
                        },
                        "total_cost": {
                          "type": "number",
                          "nullable": true
                        },
                        "cost_center": {
                          "type": "string",
                          "nullable": true
                        },
                        "at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  },
                  "requests": {
                    "type": "array",
                    "description": "Parts asked of the warehouse that it still owes (`state` open) or turned down and nobody has dismissed yet (`state` declined) — what the web card lists above the issued history. A part only requested is here, not in `items`.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "number": {
                          "type": "string",
                          "description": "The requisition number"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "open",
                            "declined"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "description": "The requisition's own status"
                        },
                        "requested_for": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "collect_at": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "picking_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "decline_reason": {
                          "type": "string",
                          "nullable": true
                        },
                        "can_cancel": {
                          "type": "boolean",
                          "description": "DELETE /service_desk/{id}/parts/requests/{requisition_id} will accept it (the warehouse has not started picking, and the token holds destructive:service_desk)."
                        },
                        "lines": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "item": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "integer"
                                  },
                                  "name": {
                                    "type": "string"
                                  }
                                }
                              },
                              "name": {
                                "type": "string"
                              },
                              "quantity": {
                                "type": "integer"
                              },
                              "issued": {
                                "type": "integer"
                              },
                              "declined": {
                                "type": "integer"
                              },
                              "remaining": {
                                "type": "integer"
                              },
                              "estimated_cost": {
                                "type": "number",
                                "description": "Cost of what is still owed"
                              }
                            }
                          }
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  },
                  "collect_points": {
                    "type": "array",
                    "description": "What POST /service_desk/{id}/parts accepts as `collect_at_location_id`. Empty when the tenant has none, and the field is not asked.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "last_collect_point_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "The collection point this person chose last, to preselect."
                  },
                  "recipients": {
                    "type": "array",
                    "description": "What POST /service_desk/{id}/parts accepts as `requested_for_id` — the caller and Service Desk staff on the job.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "me": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "project": {
                    "description": "The project this ticket bills to; null when it is not billed to one.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TicketBillingProject"
                      }
                    ]
                  },
                  "billable_projects": {
                    "type": "array",
                    "description": "What PATCH /api/v1/service_desk/{id}/project accepts — the tenant's active projects in its Procurement currency, plus the ticket's own project.",
                    "items": {
                      "$ref": "#/components/schemas/TicketBillingProject"
                    }
                  },
                  "suggested_project": {
                    "description": "While no project is set, the one the ticket's location points to; null when there is none or it is ambiguous.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TicketBillingProject"
                      }
                    ]
                  }
                }
              },
              "pending_approver": {
                "type": "object",
                "nullable": true,
                "description": "While the ticket awaits approval, the person asked to decide it (the one the approval request went to).",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "attachments": {
                "type": "array",
                "description": "Files attached to the ticket itself. Files on a comment are on that comment.",
                "items": {
                  "$ref": "#/components/schemas/ServiceDeskAttachment"
                }
              }
            }
          }
        ]
      },
      "TicketComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "content": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true,
                "description": "Null for a system-posted comment and for the anonymous requester's own replies"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "internal": {
            "type": "boolean"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceDeskAttachment"
            }
          },
          "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",
            "nullable": true,
            "description": "The type's default priority; null when the type sets none"
          },
          "service_department_id": {
            "type": "integer",
            "nullable": true
          },
          "form": {
            "$ref": "#/components/schemas/ServiceDeskIntakeTypeRules"
          }
        }
      },
      "KbSearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ],
            "description": "An integer for a `help_article` result — open it with\n`GET /service_desk/kb/articles/{article_id}`. Other sources carry a\nprefixed string id (e.g. `kb-entry-13` for a `kb_entry`) and are\nopened through `url`.\n",
            "example": 558
          },
          "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",
            "nullable": true,
            "description": "Normalized relevance in [0, 1]; null when browsing"
          },
          "source": {
            "type": "string",
            "description": "Where the result came from",
            "example": "help_article"
          },
          "status": {
            "type": "string",
            "description": "`help_article` results only. The article's workflow status — an open\nset of strings (draft, pending_review, approved, published, rejected,\narchived today). This list returns published articles only.\n",
            "example": "published"
          },
          "status_label": {
            "type": "string",
            "description": "`help_article` results only. The status as a person reads it (\"Published\").",
            "example": "Published"
          },
          "view_count": {
            "type": "integer",
            "description": "`help_article` results only."
          },
          "ratings": {
            "type": "object",
            "description": "`help_article` results only. \"Was this helpful?\" votes; the web's\nHelpfulness is helpful / (helpful + not_helpful).\n",
            "properties": {
              "helpful": {
                "type": "integer"
              },
              "not_helpful": {
                "type": "integer"
              }
            }
          }
        }
      },
      "AskAiMessageResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "context_app": {
            "type": "object",
            "nullable": true,
            "description": "Present only when the request sent `app_slug`. The app the turn is\nbound to, or null when the slug was not honoured (unknown, not open\nto this person, or no agent for it). Show an \"Asking about …\"\nindicator only when this is non-null.\n",
            "properties": {
              "slug": {
                "type": "string",
                "example": "tasks"
              },
              "name": {
                "type": "string",
                "example": "Tasks"
              }
            }
          },
          "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.",
            "description": "The message text. An answer grounded in company knowledge lists its\nsources only in `metadata.provenance.sources` (or, for rows that nest\nit, `metadata.metadata.provenance.sources`) — its text carries no\n\"Sources\" list, so render that list.\n"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "ai_generated": {
            "type": "boolean",
            "example": true,
            "description": "Machine-readable AI-output mark (EU AI Act Art. 50(2)): true on a\nstored assistant answer, false on the person's own words. The web\ntranscript's `data-ai-generated` attribute and the live `complete`\nframe say the same.\n"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Allowlisted message metadata (see the Ask AI guide, \"Message\nmetadata\"). The row's own keys may sit one level down under\n`metadata`; read each key at its current path.\n",
            "properties": {
              "provenance": {
                "$ref": "#/components/schemas/AnswerProvenance"
              },
              "turn_id": {
                "type": "string",
                "description": "Exact answer identity for feedback and correction requests."
              },
              "feedback_allowed": {
                "type": "boolean",
                "description": "False when the caller cannot submit feedback, including read-only tokens and tool-confirmation outcome rows; hide rating controls when false."
              },
              "correctable": {
                "type": "boolean",
                "description": "False when this answer cannot accept a knowledge correction."
              },
              "answer_feedback": {
                "type": "object",
                "description": "The caller's saved feedback. Staff review fields are never included.",
                "properties": {
                  "rating": {
                    "type": "string",
                    "enum": [
                      "up",
                      "down"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "incorrect",
                      "missing",
                      "off_topic",
                      "technical"
                    ]
                  },
                  "comment": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              },
              "escalated_label": {
                "type": "string",
                "description": "Present on an answer already escalated with \"Get human help\":\nwhere it went, in the web's own words (\"Sent to support (#123)\non Oct 12\"), in the tenant's language. An escalated answer\ncarries no `escalation_offer` — do not offer the button again.\n",
                "example": "Sent to support (#123) on Oct 12"
              },
              "escalated_ticket_url": {
                "type": "string",
                "format": "uri",
                "description": "The escalation's Service Desk ticket page (absolute URL), for\nlinking `escalated_label` as the web and phone do. Present only\nwhen the answer went to a ticket and Service Desk opens for\nthis person.\n",
                "example": "https://acme.workforce.mangoapps.com/apps/service-desk/123"
              }
            }
          }
        }
      },
      "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"
              },
              "cable_token": {
                "type": "string",
                "nullable": true,
                "description": "Short-lived ActionCable credential scoped to `VoiceRealtimeChannel` for\nTHIS `session_id` only. Connect with `wss://<host>/cable?cable_token=…`\nand subscribe `{ channel: \"VoiceRealtimeChannel\", session_id }`; any other\nchannel or session is rejected. `null` (the call is still 200) when the\nserver could not store it — end the session and mint again. The same value\nis also at `connections.actioncable.cable_token` for older clients.\n"
              },
              "cable_token_expires_in": {
                "type": "integer",
                "nullable": true,
                "description": "Seconds the cable token lives — the session cap (900 with the default 15-minute cap).",
                "example": 900
              },
              "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
              }
            }
          }
        }
      },
      "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"
                }
              }
            }
          },
          "reactions": {
            "$ref": "#/components/schemas/MessagingReactions"
          },
          "link_preview": {
            "type": "object",
            "nullable": true,
            "description": "The first link's card, made on the server after the message is sent; null until then, when the page has nothing to show, or while the Messages \"Link previews\" setting is off.",
            "properties": {
              "url": {
                "type": "string"
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "Our own copy of the page's image, on this host — never the linked site's address."
              },
              "site_name": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "MessagingThread": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "one_on_one",
              "group"
            ]
          },
          "title": {
            "type": "string"
          },
          "subject": {
            "type": "string",
            "nullable": true,
            "description": "The conversation's subject line (\"Fwd: …\" after a forward); null when none was given.\n"
          },
          "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"
                }
              },
              "recent_changes": {
                "type": "array",
                "description": "A group's own history, oldest first (the latest 20 inside the retention window): who renamed it, added people or left. Recorded from 2026-10-26; empty for a one-to-one conversation.",
                "items": {
                  "type": "object",
                  "properties": {
                    "action": {
                      "type": "string",
                      "enum": [
                        "renamed",
                        "added",
                        "left"
                      ]
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "by_user_id": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Null when the account was erased."
                    },
                    "renamed_from": {
                      "type": "string",
                      "nullable": true
                    },
                    "renamed_to": {
                      "type": "string",
                      "nullable": true
                    },
                    "added_user_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "text": {
                      "type": "string",
                      "description": "The sentence the web and phone conversation draw, in the reader's language."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "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,
            "description": "The room's own name: a channel's, a named group's. Null for a direct message and for a group from before names were required — draw `display_name`."
          },
          "display_name": {
            "type": "string",
            "description": "The title to show, never null, in the caller's language: `name` when set; a group with none by its people where the caller may see who is in it (`can_see_members`) — the first three still in it and how many more, \"Ann, Bo, Cy and 2 more\", as the web and phone lists name it; a direct message by the other person; else \"Untitled group\", \"Untitled channel\" or \"Direct message\"."
          },
          "topic": {
            "type": "string",
            "nullable": true
          },
          "avatar": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatChannelAvatar"
              }
            ],
            "nullable": true,
            "description": "A channel's own picture when it has no photo (`image_url`): one emoji on one colour. Null for a group or a direct message, and for a channel whose picture has not been picked yet — draw your own # / lock then."
          },
          "created_by_id": {
            "type": "integer",
            "nullable": true,
            "description": "Who created the room. The creator of a group or channel is offered no Leave (the web hides it too); on a note to self it is its owner. `is_room_admin` cannot stand in — a promoted admin is not the creator."
          },
          "is_note_to_self": {
            "type": "boolean",
            "description": "The caller's own conversation: a direct room with no other person, so every `other_user_*` field is null. Listed only when rooms#index is asked `include_note_to_self`."
          },
          "is_room_admin": {
            "type": "boolean",
            "description": "The caller is an admin of this room (the person's standing; whether this request may edit is `can_edit_room`)."
          },
          "is_team_admin": {
            "type": "boolean",
            "description": "Same value as `is_room_admin` (the messenger's older name)."
          },
          "can_edit_room": {
            "type": "boolean",
            "description": "This request may change the room (PATCH /chat/rooms/{id}): a channel when `can_edit_details`; otherwise a group the caller is an admin of, with a token holding `write:chat`. Never a direct message. Draw Save only when true — a read-only token gets 403."
          },
          "can_see_members": {
            "type": "boolean",
            "description": "May see who is in the room (the room's `can_view_member` setting, or an admin). `member_count` is null when false."
          },
          "can_send_im": {
            "type": "boolean",
            "description": "May post here; false with a reason in `send_restriction`, and for a token without `write:chat`."
          },
          "can_add_members": {
            "type": "boolean",
            "description": "May add people (`other_member_invite`, or an admin); never in a direct message, an archived room or a member-synced channel."
          },
          "can_pin": {
            "type": "boolean",
            "description": "May pin a message: whoever may post, plus the room's admins; never in an archived room."
          },
          "can_broadcast_mentions": {
            "type": "boolean",
            "description": "May use @channel / @here / @group: never in a direct message or where the caller cannot post; a room admin always; else whoever the company lets use them."
          },
          "has_read_receipt_messages": {
            "type": "boolean",
            "description": "A read-receipt message here still waits for the caller's acknowledgement — the room stays under Unread until it is acknowledged, even once `unread_count` is 0."
          },
          "read_receipt_message_count": {
            "type": "integer",
            "description": "Read-receipt messages here the caller has not acknowledged."
          },
          "other_member_invite": {
            "type": "string",
            "enum": [
              "Y",
              "N"
            ],
            "description": "The room's stored setting — see ChatRoomPermissionCodes."
          },
          "can_view_member": {
            "type": "string",
            "enum": [
              "B",
              "G"
            ],
            "description": "The room's stored setting — see ChatRoomPermissionCodes."
          },
          "im_permission": {
            "type": "string",
            "enum": [
              "B",
              "G",
              "S"
            ],
            "description": "The room's stored setting — see ChatRoomPermissionCodes."
          },
          "important_msg_sender": {
            "type": "string",
            "enum": [
              "B",
              "G",
              "S"
            ],
            "description": "The room's stored setting — see ChatRoomPermissionCodes."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the conversation began. Where its messages expire (`retention_days`) and it is older than that many days, the oldest message shown may not be its first: the web and phone history then start with \"Older messages are deleted after 90 days (your company's retention setting).\" instead of \"This is the beginning of the conversation.\""
          },
          "is_public": {
            "type": "boolean",
            "description": "A channel listed in Browse Channels and open to join; false for every DM and group."
          },
          "archived": {
            "type": "boolean",
            "description": "An archived channel is read-only (D11)."
          },
          "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"
          },
          "has_self_mention": {
            "type": "boolean",
            "description": "The caller has an unread message here that @mentions them — by name, or an @channel / @here / group mention that reached them — by the rule the Mentions view marks \"New\" with: past their read position (for a reply only its thread shows, past their position in that thread), in a room they are still in, in a message not deleted. False for someone not in the room, and in an archived room (as on the Home page's Chat card)."
          },
          "unread_mention_count": {
            "type": "integer",
            "description": "How many such messages (0 when has_self_mention is false)."
          },
          "can_send_important_msg": {
            "type": "boolean"
          },
          "can_request_read_receipt": {
            "type": "boolean",
            "description": "May ask for a read receipt on a new message here."
          },
          "ack_restriction": {
            "type": "object",
            "nullable": true,
            "description": "Why `can_send_important_msg` is false — show the Important control disabled with `message`. The first that applies, in the order a send is refused: posting itself (the same code and sentence as `send_restriction`, including `read_only_token`), then `ack_not_allowed_in_channel` (Important messages and read receipts are for direct messages, groups and private channels of up to 50 people — D12), then `important_not_allowed` (the company's Important messages switch is off, or this room's \"Who can send important messages\" tier leaves the caller out). Null while the caller may send one, and for someone not in the room.",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "ack_not_allowed_in_channel",
                  "important_not_allowed",
                  "information_barrier",
                  "im_not_allowed",
                  "read_only_token"
                ]
              },
              "message": {
                "type": "string",
                "example": "Important messages and read receipts are for private channels of up to 50 people."
              }
            }
          },
          "read_receipt_restriction": {
            "type": "object",
            "nullable": true,
            "description": "Why `can_request_read_receipt` is false, the same way: posting itself, then `ack_not_allowed_in_channel`, then `read_receipts_not_allowed` (the company's read-receipt switch is off) or `read_receipts_not_allowed_here` (this room's tier leaves the caller out). The two switches are separate, so this can differ from `ack_restriction`. Null while the caller may ask, and for someone not in the room.",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "ack_not_allowed_in_channel",
                  "read_receipts_not_allowed",
                  "read_receipts_not_allowed_here",
                  "information_barrier",
                  "im_not_allowed",
                  "read_only_token"
                ]
              },
              "message": {
                "type": "string",
                "example": "Read-receipt requests are not enabled for your organization."
              }
            }
          },
          "can_delete_self_message": {
            "type": "boolean"
          },
          "can_delete_other_message": {
            "type": "boolean"
          },
          "can_reply_in_thread": {
            "type": "boolean",
            "description": "Channels only: may reply in a thread (D8)."
          },
          "can_archive": {
            "type": "boolean",
            "description": "May archive / restore this channel (D11; needs destructive:chat)."
          },
          "can_make_private": {
            "type": "boolean",
            "description": "May make this public channel private (D9)."
          },
          "can_make_public": {
            "type": "boolean",
            "description": "May make this private channel public — a company Chat admin only (D9)."
          },
          "can_edit_details": {
            "type": "boolean",
            "description": "May change this channel's name and topic."
          },
          "can_delete_for_everyone": {
            "type": "boolean",
            "description": "May delete it for everyone (needs destructive:chat)."
          },
          "can_close_polls": {
            "type": "boolean",
            "description": "May close anyone's poll here — a room admin, not in an archived channel (a poll's author may always close their own; needs destructive:chat)."
          },
          "thread_replies": {
            "type": "string",
            "enum": [
              "B",
              "G"
            ],
            "description": "Who may reply in threads (channels): B any member, G admins only."
          },
          "ask_ai_proactive_enabled": {
            "type": "boolean",
            "description": "The room is opted in to Ask AI's unprompted suggestions (set with PATCH /chat/rooms/{id})."
          },
          "ask_ai_proactive_detectors": {
            "type": "object",
            "description": "Which suggestions it makes when opted in.",
            "properties": {
              "unanswered_question": {
                "type": "boolean"
              },
              "meeting_intent": {
                "type": "boolean"
              },
              "commitment": {
                "type": "boolean"
              }
            }
          },
          "image_url": {
            "type": "string",
            "nullable": true,
            "description": "Group/channel photo."
          },
          "image_updated_at": {
            "type": "integer",
            "nullable": true
          },
          "service_account_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "approvals",
              "notifications",
              null
            ],
            "description": "A direct message with one of Chat's service accounts: `approvals` (the Needs you conversation — what is waiting on the caller, as cards they can answer from: approvals, acknowledgements and sign-offs) or `notifications` (copies of the caller's bell notices, when they asked for them). Read-only for the caller — `can_send_im` is false with the reason in `send_restriction`; draw it as the app's own conversation, not a person. Null for every other room."
          },
          "notify_level": {
            "type": "string",
            "nullable": true,
            "enum": [
              "all",
              "mentions",
              "none",
              null
            ],
            "description": "The caller's notification level in force (a channel of more than 50 members defaults to mentions). Null when not a member."
          },
          "notify_level_setting": {
            "type": "string",
            "nullable": true,
            "enum": [
              "all",
              "mentions",
              "none",
              null
            ],
            "description": "What the caller chose; null follows the room's default."
          },
          "default_notify_level": {
            "type": "string",
            "nullable": true,
            "enum": [
              "all",
              "mentions",
              null
            ],
            "description": "The level the caller has here with no setting of their own — mentions for a channel of more than 50 members, else all; the same answer PATCH /chat/rooms/{id}/notifications gives. Null when not a member."
          },
          "walkie_talkie": {
            "type": "boolean",
            "description": "The caller's walkie-talkie switch here (PATCH /chat/rooms/{id}/walkie_talkie): on, push-to-talk clips play as they arrive. False when not a member."
          },
          "draft_preview": {
            "type": "string",
            "nullable": true,
            "description": "The caller's unsent draft here, as a list shows it (\"Draft: …\"); null without one."
          },
          "org_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "group",
              "location",
              "team",
              "shift",
              null
            ],
            "description": "What made this channel: a group an admin picked, a location, a manager's team, or a location's who's-on-shift-today channel. Null for one made by hand."
          },
          "synced_from": {
            "type": "object",
            "nullable": true,
            "description": "While the members are synced from that source (people join and leave with it; Add people is refused, and so are Leave and Remove for anyone the source holds — 422 `org_synced`; `can_add_members` is false, and `can_leave` is false for someone the source holds). Null otherwise, including after the setting that made it is turned off, after a location left the Location channels level, and after Stop syncing (DELETE /chat/rooms/{id}/org_sync).",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "group",
                  "location",
                  "team",
                  "shift"
                ]
              },
              "id": {
                "type": "integer",
                "description": "The group's id for a group; the location's or the manager's for the others."
              },
              "name": {
                "type": "string",
                "description": "Where the members come from, as a sentence fragment (\"Store 412\", \"Dana Lee's team\")."
              },
              "source_empty_since": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the group it follows was found holding nobody: the channel kept its members since, and people join and leave again once the group holds people. Null otherwise (always null for a location, team or shift)."
              }
            }
          },
          "org_sync_stopped": {
            "type": "boolean",
            "description": "The group it followed was deleted: it keeps its members and no longer syncs."
          },
          "can_leave": {
            "type": "boolean",
            "description": "May leave this room (false in a DM, and in a synced channel for someone its source holds — a former manager kept as admin, or someone added by hand, may leave; creator and last-admin refusals still apply). Needs destructive:chat."
          },
          "can_stop_org_sync": {
            "type": "boolean",
            "description": "May stop this channel syncing its members (DELETE /chat/rooms/{id}/org_sync): the channel's admins and Chat admins, on a channel that follows a source and is not archived."
          },
          "can_manage_webhooks": {
            "type": "boolean",
            "description": "May list, add and revoke this channel's incoming webhooks (/chat/rooms/{id}/webhooks): the channel's admins and Chat admins; on an archived channel, listing and revoking only. Needs write:chat."
          },
          "send_restriction": {
            "type": "object",
            "nullable": true,
            "description": "Why `can_send_im` is false — the sentence the web and the phone show in place of the message box: the room's own reason first, else the token's. Null while the caller may post, and for someone not in the room.",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "information_barrier",
                  "im_not_allowed",
                  "read_only_token"
                ],
                "description": "`information_barrier` for a direct message the company's information barriers keep apart (sends, reactions, pins and edits are refused; deleting your own messages still works); `im_not_allowed` for every other reason the room refuses a post; `read_only_token` when the room would take the post but the caller's token has no `write:chat`."
              },
              "message": {
                "type": "string",
                "example": "Your company's policy doesn't allow messages between you and Dana Lee."
              }
            }
          },
          "on_shift_count": {
            "type": "integer",
            "nullable": true,
            "description": "Members on shift now (GET /chat/rooms/{id}/on_shift lists them). Null for a non-member and where the roster is kept to admins."
          },
          "retention_days": {
            "type": "integer",
            "nullable": true,
            "description": "Messages here are deleted after this many days (say so once, e.g. \"Messages older than 90 days are deleted.\"): hidden from everyone, then permanently deleted 30 days later. Null when this kind of conversation keeps them forever. The same for every viewer, whether or not the room is on legal hold."
          },
          "on_legal_hold": {
            "type": "boolean",
            "nullable": true,
            "description": "The room, one of its messages, or someone in it is on legal hold: nothing in it is permanently deleted (messages past the retention window are still hidden) and it can't be deleted for everyone. Business (company) admins only; null for everyone else."
          },
          "held_by_other": {
            "type": "boolean",
            "nullable": true,
            "description": "Held, with no hold on the room itself: a hold on someone in it, or on one of its messages, keeps it — lifted on Admin › Retention & Legal Holds, not from the conversation. The web's \"On legal hold\" badge says which in its tooltip. False when nothing holds it or the hold is on the room; business (company) admins only, null for everyone else."
          },
          "delete_restriction": {
            "type": "object",
            "nullable": true,
            "description": "Why delete for everyone is refused to someone who could otherwise delete it — the room is held. `code` is `legal_hold` for a business admin (the message names the hold and where to lift it) and `not_deletable` for anyone else (the message names no hold). Show the delete control disabled with `message`. Null otherwise, and for a token without destructive:chat.",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "legal_hold",
                  "not_deletable"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          },
          "can_manage_legal_hold": {
            "type": "boolean",
            "description": "The conversation's own menu offers placing or lifting its legal hold (POST / DELETE /chat/rooms/{id}/legal_hold): a business admin in it, under write:chat. A business admin who is not in it may hold it by its id too."
          }
        }
      },
      "ChatRoomPermissionCodes": {
        "type": "object",
        "description": "Any of the five settings; a key left out is unchanged (or, on a new room, its default). Codes are upper-case, exactly as listed. \"Admins\" means room admins and business admins.",
        "properties": {
          "other_member_invite": {
            "type": "string",
            "enum": [
              "Y",
              "N"
            ],
            "description": "Who can add new members: Y any member, N admins only."
          },
          "can_view_member": {
            "type": "string",
            "enum": [
              "B",
              "G"
            ],
            "description": "Who can see the member list: B any member, G admins only."
          },
          "im_permission": {
            "type": "string",
            "enum": [
              "B",
              "G",
              "S"
            ],
            "description": "Who can send messages: B any member, G admins only, S business admins only."
          },
          "important_msg_sender": {
            "type": "string",
            "enum": [
              "B",
              "G",
              "S"
            ],
            "description": "Who can send important messages and request read receipts: B any member, G admins only, S business admins only."
          },
          "thread_replies": {
            "type": "string",
            "enum": [
              "B",
              "G"
            ],
            "description": "Who can reply in threads: B any member, G admins only. Read only in a channel."
          }
        }
      },
      "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
          },
          "playback_url": {
            "type": "string",
            "nullable": true,
            "description": "Signed URL of the 720p H.264 MP4 playback rendition, when the video has one. Players should prefer it and fall back to the original (`storage_url` / `url`). Null while processing, and for uploads that already play everywhere."
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true,
            "deprecated": true,
            "description": "No longer produced; present only on videos transcoded before 2026-09."
          },
          "subtitle_url": {
            "type": "string",
            "nullable": true,
            "description": "Video with subtitles ready."
          },
          "duration_ms": {
            "type": "integer",
            "nullable": true,
            "example": 7400,
            "description": "A voice note's length in milliseconds, as its recorder measured it (the `duration_ms` sent with `POST …/voice_notes`) — show it instead of the audio element's own, which a WebM recording leaves at Infinity until it has played through. Null for every other attachment, and for voice notes sent without one (or before it was kept). The `message:created` payload's attachments carry it too."
          }
        }
      },
      "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"
          },
          "user_name": {
            "type": "string",
            "nullable": true,
            "description": "The sender's display name; null when the sender's account is gone. Mirrors the `message:created` Pusher payload."
          },
          "user_avatar_url": {
            "type": "string",
            "nullable": true,
            "description": "The sender's avatar image URL; null when the sender has no photo or the account is gone."
          },
          "user_avatar_updated_at": {
            "type": "integer",
            "nullable": true,
            "description": "Epoch seconds the sender's avatar last changed, for cache-busting; null without a photo.",
            "example": 1761600000
          },
          "user_designation": {
            "type": "string",
            "nullable": true,
            "description": "The sender's job title as this business shows it — their membership's title (set by the profile's Job Title select), else the profile's own. Present only where the People directory shows job titles to the caller; the caller's own title follows that field's Visible-to level. Null when hidden or when the sender has no title."
          },
          "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
          },
          "in_thread": {
            "type": "boolean",
            "description": "A reply in a channel — it belongs to its root's thread (D1). A client without threads shows it as the inline quote it always was."
          },
          "thread_only": {
            "type": "boolean",
            "description": "The reply was kept in its thread (not also sent to the channel timeline)."
          },
          "thread": {
            "type": "object",
            "description": "With `threads=true`, on each root in a channel — its thread line.",
            "properties": {
              "reply_count": {
                "type": "integer"
              },
              "last_reply_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "repliers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ChatUserSummary"
                }
              },
              "following": {
                "type": "boolean"
              }
            }
          },
          "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,
            "description": "Set on a deleted message's tombstone, which carries no body, attachments (`[]`), link_preview (null) or mention_groups (`[]`)."
          },
          "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",
            "description": "A server notice (\"Ann has added Bo to this chat.\") — draw it as a centered line, not a bubble. Its `body` is already said for the caller: in their display language, and \"You have …\" / \"… has added you …\" where it names them. `system_kind` and `system_params` say what happened, for a client that says it itself."
          },
          "system_kind": {
            "type": "string",
            "nullable": true,
            "description": "What a system notice records; null on a person's message and on a notice written before kinds were stored (its `body` is then the stored English). A client that does not know a kind shows `body`.",
            "enum": [
              "chat_created",
              "members_added",
              "member_left",
              "member_removed",
              "admin_added",
              "admin_removed",
              "admin_succession",
              "chat_renamed",
              "channel_created",
              "channel_members_added",
              "channel_joined",
              "channel_archived",
              "channel_restored",
              "channel_made_public",
              "channel_made_private",
              "channel_topic_set",
              "channel_topic_cleared",
              "group_converted",
              "permission_changed",
              "message_pinned",
              "org_sync_stopped",
              "org_detached",
              "org_reattached",
              "org_synced",
              "org_channel_intro",
              "huddle_recording",
              "webhook_added",
              "webhook_removed",
              "webhook_renamed",
              null
            ]
          },
          "system_params": {
            "type": "object",
            "nullable": true,
            "description": "The facts a system notice is said from — present keys depend on `system_kind`. People carry the name they had when it happened.",
            "properties": {
              "actor": {
                "type": "object",
                "description": "Who did it (absent when nobody did — admin_succession, org lines, an automatic group_converted).",
                "properties": {
                  "id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "member": {
                "type": "object",
                "description": "The one person it is about (member_left, member_removed, admin_added, admin_removed, admin_succession).",
                "properties": {
                  "id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "members": {
                "type": "array",
                "description": "The first five people it lists (members_added, channel_members_added).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              },
              "member_ids": {
                "type": "array",
                "description": "Every listed person's id, up to 200 — to say \"you\" for a reader past the first five.",
                "items": {
                  "type": "integer"
                }
              },
              "member_count": {
                "type": "integer",
                "description": "How many people it lists in all (\"and N others\")."
              },
              "from": {
                "type": "string",
                "description": "chat_renamed — the old name (absent when it had none)."
              },
              "to": {
                "type": "string",
                "description": "chat_renamed — the new name."
              },
              "visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "private"
                ],
                "description": "channel_created."
              },
              "topic": {
                "type": "string",
                "description": "channel_topic_set."
              },
              "setting": {
                "type": "string",
                "description": "permission_changed — the setting key (other_member_invite, can_view_member, im_permission, important_msg_sender, thread_replies)."
              },
              "value": {
                "type": "string",
                "description": "permission_changed — its new value code (the codes of PATCH /chat/rooms/{id}/permissions)."
              },
              "channel_name": {
                "type": "string",
                "description": "group_converted — the channel's name, without the '#'."
              },
              "source": {
                "type": "object",
                "description": "What a synced channel follows (org_* kinds): `kind` group, location, shift (who's on shift at a location) or team (a manager's), and its name.",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "added": {
                "type": "integer",
                "description": "org_synced — people who joined."
              },
              "removed": {
                "type": "integer",
                "description": "org_synced — people who left."
              },
              "webhook_name": {
                "type": "string",
                "description": "webhook_added, webhook_removed, webhook_renamed — the incoming webhook's name (POST /chat/rooms/{id}/webhooks); webhook_renamed's new one."
              },
              "previous_webhook_name": {
                "type": "string",
                "description": "webhook_renamed — the name it had before (PATCH /chat/rooms/{id}/webhooks/{webhook_id})."
              }
            }
          },
          "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)."
          },
          "link_preview": {
            "type": "object",
            "nullable": true,
            "description": "The first link's card; null when there is none, the sender removed it, or the body is masked. Arrives after the message, over `message:updated` with `link_preview_only`.",
            "properties": {
              "url": {
                "type": "string"
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "https only."
              },
              "site_name": {
                "type": "string",
                "nullable": true
              },
              "source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "share"
                ],
                "description": "\"share\" on the card under a record shared from OneMango (a course, a page, a proud moment): draw its picture across the card, not as a thumbnail."
              }
            }
          },
          "read_percent": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "On the caller's own message in a room of more than 50 members: the share of the other members who have read up to it (rounded down). Null elsewhere: on Important / read-receipt messages (they carry their own counts), on a reply kept in its thread, and while the company's \"Show who has seen a message\" switch (seen_by_enabled) is off."
          },
          "pinned": {
            "type": "boolean",
            "description": "Pinned to its conversation (POST …/pin)."
          },
          "pinned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "pinned_by": {
            "nullable": true,
            "description": "Who pinned it; null when not pinned.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatUserSummary"
              }
            ]
          },
          "permalink": {
            "type": "string",
            "description": "The link to this message — absolute, on this company's host: `/apps/chat/rooms/{room_id}?message={id}#message-{id}`. The web opens the conversation at it (its thread, for a reply kept in one); a phone is sent to the phone view of the same message."
          },
          "saved": {
            "type": "boolean",
            "description": "The CALLER saved it for later (POST …/save)."
          },
          "recorder_recording_id": {
            "type": "integer",
            "nullable": true,
            "description": "The Recorder recording a \"Send to chat\" card is for (open it with `GET /recorder/recordings/{id}`), so a client draws the card from the message instead of parsing its link. The same key the `message:created` push carries. Null on every other message, and withheld (null) with the body — on a deleted message, and until the reader acknowledges a read-receipt request.",
            "example": 4821
          },
          "recorder_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "video",
              "screenshot",
              null
            ],
            "description": "Which card to draw for `recorder_recording_id`; null exactly when it is.",
            "example": "video"
          },
          "forwarded_from": {
            "nullable": true,
            "description": "Set on a message posted by Forward (`POST …/messages/{id}/forward`): the message it quotes — the forwarded message itself when that was a forward too — and the forward's own `body` cut into the note and the quoted words. The same for every reader, and the same object on `message:created`. Null on every other message, on a removed one, and on forwards sent before it existed (their `body` is the same plain text). A message with it cannot be edited (403 `forwarded_message`).",
            "type": "object",
            "properties": {
              "message_id": {
                "type": "integer",
                "description": "The message forwarded."
              },
              "room_id": {
                "type": "integer",
                "description": "The conversation it was said in."
              },
              "author": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "description": "The name the body's \"Forwarded from\" line gives them."
                  }
                }
              },
              "room_label": {
                "type": "string",
                "description": "Where it was said, never naming a private conversation: `#name` for a public channel, \"a private channel\", \"a group\", \"a direct message\", or \"a conversation\" when the room is gone. English; a reader who is in `room_id` can show that room's own name."
              },
              "room_kind": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "public_channel",
                  "private_channel",
                  "group",
                  "direct"
                ],
                "description": "What kind of conversation `room_id` is, to word `room_label` in the reader's language; null when the room is gone."
              },
              "note": {
                "type": "string",
                "description": "The forwarder's note (\"\" when they wrote none) — the start of `body`."
              },
              "quoted_text": {
                "type": "string",
                "description": "The original's words as `body` carries them: plain text, people as \"@Name\", broadcast words disarmed — without the \"Forwarded from\" line and the attachments line."
              },
              "attachment_count": {
                "type": "integer",
                "description": "Attachments on the original (they stay with it)."
              },
              "permalink": {
                "type": "string",
                "description": "The original's link on this company's host — `/apps/chat/rooms/{room_id}?message={message_id}#message-{message_id}`."
              }
            }
          },
          "escalation": {
            "nullable": true,
            "description": "On the CALLER's own Important message with a backup — who is told if it isn't confirmed in time, and what happened. Null for everyone else. Changes arrive over `message:updated` with `update_kind \"escalation\"` (ids only — fetch the message again).",
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatMessageEscalation"
              }
            ]
          },
          "card": {
            "nullable": true,
            "description": "A card on a message from the Needs you service account (its DM with the caller) — an approval, or an Inbox ask to acknowledge or sign; null on every other message. Changes arrive over `message:updated` with `update_kind \"card\"`.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatMessageCard"
              }
            ]
          },
          "transcript": {
            "type": "object",
            "nullable": true,
            "description": "A voice note's words — null on every other message (and while masked or deleted). Arrives later over `message:updated` with `update_kind \"transcript\"`.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "completed",
                  "failed"
                ]
              },
              "text": {
                "type": "string",
                "nullable": true
              },
              "error": {
                "type": "string",
                "nullable": true,
                "description": "A sentence for the person when there is one (no speech heard",
                "the AI usage cap).": null
              }
            }
          },
          "poll": {
            "nullable": true,
            "description": "The poll on a poll message (its body is the question); null otherwise. Changes arrive over `message:updated` with `update_kind \"poll\"`.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatPoll"
              }
            ]
          },
          "mention_groups": {
            "type": "array",
            "description": "The groups the body @mentions (`@[Name](group:id)`) by their real names — badge these; [] when none or masked. `@channel` and `@here` are plain words in the body.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "push_to_talk": {
            "type": "boolean",
            "description": "A voice note sent by holding the talk button (walkie-talkie). The message:created payload carries the same key; a listener whose room `walkie_talkie` is on plays it once."
          }
        }
      },
      "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
          },
          "walkie_talkie": {
            "type": "boolean",
            "nullable": true,
            "description": "The walkie-talkie switch — on the caller's own row only (a private listening preference, like muted_until); null on everyone else's."
          },
          "last_read_message_id": {
            "type": "integer",
            "nullable": true,
            "description": "How far this member has read — what a client draws \"Seen by\" from\n(Chat Wave 4: in a group up to 50 as well as a 1:1 room). Null in a\ngroup or channel for a member a works council hides by name, and\nnull for everyone but the caller while the company's \"Show who has\nseen a message\" switch (`chat_settings.seen_by_enabled`) is off —\nsee `read_positions_hidden` on the list response.\n"
          },
          "last_read_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When `last_read_message_id` last moved forward — the time of a 1:1\nroom's \"Seen by Ann at 3:15 PM\", with its day named when it is not\nthe message's own (\"Seen by Ann today at …\"), as the web and the\nphone draw it after a reload. Null exactly where\n`last_read_message_id` is, and also for a read from before the time\nwas stored (Oct 2026) or a position moved back by Mark unread\n(the line then says \"Seen by Ann\").\n"
          },
          "active_in_business": {
            "type": "boolean",
            "description": "On the member list only. False for a person no longer active in this\norganization: they keep their membership row (history keeps its\nauthor), cannot be made an admin (`member_inactive`) and are not\nnotified when removed.\n"
          },
          "user": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatUserSummary"
              },
              {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "nullable": true,
                    "description": "A coworker's email only where the People directory shows the caller email; null otherwise. The caller's own row always carries theirs."
                  }
                }
              }
            ]
          }
        }
      },
      "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
          },
          "playback_url": {
            "type": "string",
            "nullable": true,
            "description": "Signed URL of the 720p H.264 MP4 playback rendition, when the video has one. Players should prefer it and fall back to the original (`storage_url` / `url`). Null while processing, and for uploads that already play everywhere."
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true,
            "deprecated": true,
            "description": "No longer produced; present only on videos transcoded before 2026-09."
          }
        }
      },
      "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"
          },
          "seen_by_enabled": {
            "type": "boolean",
            "description": "\"Show who has seen a message\" (default true). Off: no \"Seen by\" and no seen tick (only Sent / Delivered), no `message:read` events, no `read_percent`, and every other member's `last_read_message_id` is null on the member list. Important / read-receipt acknowledgements are not affected. On: a client draws \"Seen by\" in a 1:1 room and in a group of up to 50 from the members' `last_read_message_id` — the names of those who have read up to the message, up to three then \"+N\", on the sender's newest own message that someone has read."
          },
          "important_messages_enabled": {
            "type": "boolean"
          },
          "read_receipt_requests_enabled": {
            "type": "boolean"
          },
          "link_previews_enabled": {
            "type": "boolean",
            "description": "Chat Wave 2: a card for the first link in a message."
          },
          "incoming_webhooks_enabled": {
            "type": "boolean",
            "description": "A channel's admins may add incoming webhooks (GET/POST /chat/rooms/{id}/webhooks). Off: no webhook can be added and every webhook URL answers 403 action_prohibited; the webhooks are kept. Default true."
          },
          "semantic_search_enabled": {
            "type": "boolean"
          },
          "chat_video_subtitles_enabled": {
            "type": "boolean"
          },
          "working_hours_start": {
            "type": "integer"
          },
          "working_hours_end": {
            "type": "integer"
          },
          "retention_days_direct": {
            "type": "integer",
            "readOnly": true,
            "description": "Days direct messages are kept (30–3650; a stored value under 30 is applied as 30); 0 = forever. Not writable through PATCH /chat/settings."
          },
          "retention_days_group": {
            "type": "integer",
            "readOnly": true,
            "description": "Days group messages are kept (30–3650; a stored value under 30 is applied as 30); 0 = forever. Not writable through PATCH /chat/settings."
          },
          "retention_days_channel": {
            "type": "integer",
            "readOnly": true,
            "description": "Days channel messages are kept (30–3650; a stored value under 30 is applied as 30); 0 = forever. Not writable through PATCH /chat/settings."
          }
        }
      },
      "ChatPoll": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "question": {
            "type": "string"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "votes": {
                  "type": "integer"
                }
              }
            }
          },
          "total_votes": {
            "type": "integer",
            "description": "Votes cast (a multiple-choice voter may cast several)."
          },
          "voter_count": {
            "type": "integer",
            "description": "People who voted."
          },
          "my_votes": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The caller's chosen option indexes ([] on a broadcast, which has no viewer)."
          },
          "closed": {
            "type": "boolean"
          },
          "allow_multiple": {
            "type": "boolean"
          },
          "author_id": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "ChatDraft": {
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "nullable": true,
            "description": "The encoded text a send would post; null with no draft."
          },
          "mention_user_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "parent_message_id": {
            "type": "integer",
            "description": "Only on a thread reply draft (`parent_message_id` asked): the root message of the thread it belongs to."
          }
        }
      },
      "ChatScheduledMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "room_id": {
            "type": "integer"
          },
          "body": {
            "type": "string"
          },
          "send_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "sending",
              "sent",
              "failed",
              "cancelled"
            ]
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Why it was not sent (status failed)."
          },
          "parent_message_id": {
            "type": "integer",
            "nullable": true
          },
          "thread_only": {
            "type": "boolean"
          },
          "sent_message_id": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ChatOwnStatus": {
        "type": "object",
        "properties": {
          "emoji": {
            "type": "string",
            "nullable": true
          },
          "text": {
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the status clears; null = it does not."
          },
          "dnd": {
            "type": "boolean",
            "description": "Notifications paused."
          },
          "dnd_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "out_of_office": {
            "type": "boolean",
            "description": "On approved leave today."
          }
        }
      },
      "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"
                },
                "code": {
                  "type": "string",
                  "description": "Always `validation_error`"
                },
                "field_name": {
                  "type": "string",
                  "description": "Present only when an error on `base` is about one named field — a\nsurvey or form question left blank (`\"<label> is required\"`). The\nquestion's key, as sent in `submission_data`.\n"
                }
              }
            }
          }
        }
      },
      "ShiftWriteRequest": {
        "type": "object",
        "required": [
          "shift"
        ],
        "properties": {
          "shift": {
            "type": "object",
            "description": "The shift's fields. On create, `name`, `start_time` and `end_time` are required. `location_id`\nis refused on create (400) and ignored on update; `needs_coverage` is computed, never sent.\nAn explicit `\"urgent\": null` is ignored (the flag is left as it was).\n",
            "properties": {
              "name": {
                "type": "string"
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "start_time": {
                "type": "string",
                "format": "date-time"
              },
              "end_time": {
                "type": "string",
                "format": "date-time"
              },
              "status": {
                "type": "string",
                "description": "Defaults to the shift's initial status when omitted"
              },
              "location_department_id": {
                "type": "integer",
                "nullable": true,
                "description": "A department of the shift's location"
              },
              "required_staff_count": {
                "type": "integer"
              },
              "urgent": {
                "type": "boolean"
              },
              "shift_label_id": {
                "type": "integer",
                "nullable": true
              },
              "notes": {
                "type": "string",
                "nullable": true
              },
              "planned_breaks_attributes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "An existing planned break to change (omit to add one)"
                    },
                    "break_type_id": {
                      "type": "integer"
                    },
                    "timing_type": {
                      "type": "string"
                    },
                    "scheduled_time": {
                      "type": "string"
                    },
                    "window_start": {
                      "type": "string"
                    },
                    "window_end": {
                      "type": "string"
                    },
                    "timing_preferences": {
                      "type": "object"
                    },
                    "is_required": {
                      "type": "boolean"
                    },
                    "_destroy": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "user_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "People to assign (members of this organization). Omit to leave the roster alone; on update,\nan empty array clears it.\n"
          }
        }
      },
      "ShiftWriteResponse": {
        "type": "object",
        "required": [
          "id",
          "name",
          "start_time",
          "end_time",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "location_id": {
            "type": "integer",
            "nullable": true
          },
          "location_department_id": {
            "type": "integer",
            "nullable": true
          },
          "schedule_id": {
            "type": "integer",
            "nullable": true
          },
          "scheduling_group_id": {
            "type": "integer",
            "nullable": true
          },
          "scheduling_sub_group_id": {
            "type": "integer",
            "nullable": true
          },
          "shift_type": {
            "type": "string",
            "nullable": true
          },
          "required_staff_count": {
            "type": "integer",
            "nullable": true
          },
          "needs_coverage": {
            "type": "boolean"
          },
          "urgent": {
            "type": "boolean"
          },
          "ends_next_day": {
            "type": "boolean",
            "nullable": true
          },
          "is_open_shift": {
            "type": "boolean",
            "nullable": true
          },
          "is_ad_hoc": {
            "type": "boolean",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "users": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "first_name": {
                  "type": "string"
                },
                "last_name": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "job_title": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "planned_breaks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "break_type_id": {
                  "type": "integer"
                },
                "timing_type": {
                  "type": "string"
                },
                "scheduled_time": {
                  "type": "string",
                  "nullable": true
                },
                "window_start": {
                  "type": "string",
                  "nullable": true
                },
                "window_end": {
                  "type": "string",
                  "nullable": true
                },
                "duration_minutes": {
                  "type": "integer",
                  "nullable": true
                },
                "is_required": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "ShiftBulkRow": {
        "type": "object",
        "description": "One row of POST /shifts/bulk_create or PUT /shifts/bulk_update (unwrapped — no `shift` key).",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "location_id": {
            "type": "integer"
          },
          "location_department_id": {
            "type": "integer",
            "nullable": true
          },
          "schedule_id": {
            "type": "integer",
            "nullable": true
          },
          "required_staff_count": {
            "type": "integer"
          },
          "urgent": {
            "type": "boolean"
          },
          "ends_next_day": {
            "type": "boolean"
          },
          "is_open_shift": {
            "type": "boolean"
          },
          "notes": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ShiftBulkResult": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "Some shifts failed to create"
          },
          "results": {
            "type": "object",
            "properties": {
              "success": {
                "type": "array",
                "description": "IDs of the shifts that were written",
                "items": {
                  "type": "integer"
                }
              },
              "failed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "bulk_update only"
                    },
                    "params": {
                      "type": "object",
                      "description": "bulk_create only — the row as received"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "code": {
                      "type": "string",
                      "description": "`externally_managed` when the shift is managed in the workforce management system"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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"
              },
              "matched_excerpt": {
                "type": "string",
                "nullable": true,
                "description": "Plain text from the page body around the first word of the query that matched, with \"…\" where it was cut. Null when only the title, description or tags matched.\n",
                "example": "…overtime is paid at 1.5× after 40 hours in a week…"
              },
              "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, and no removals).",
                "example": false
              },
              "vote_change_allowed": {
                "type": "boolean",
                "description": "Whether the caller may remove their upvote from THIS idea now: the \"Allow removing an upvote\" workspace setting is on AND the idea's voting is not closed. When false, DELETE /ideas/{idea_id}/vote is refused (403 vote_change_disabled, or 422 voting_closed) — 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"
          }
        }
      },
      "CurrentUser": {
        "description": "`GET /users/me` — the caller's own record (Api::V1::UsersController\n#build_base_user_data). Not the directory row (`User`): `department` here\nis an object, and `role`, `timezone`, `language` and the permission and\nfeature-flag lists are the caller's own.\n",
        "type": "object",
        "required": [
          "id",
          "first_name",
          "last_name",
          "full_name",
          "active",
          "department",
          "language",
          "language_set"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true,
            "description": "Null for a person with no email address."
          },
          "first_name": {
            "type": "string",
            "nullable": true,
            "example": "John"
          },
          "last_name": {
            "type": "string",
            "nullable": true,
            "example": "Doe"
          },
          "full_name": {
            "type": "string",
            "example": "John Doe"
          },
          "role": {
            "type": "string",
            "description": "The caller's role name; an empty string when they have none.",
            "example": "employee"
          },
          "active": {
            "type": "boolean"
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "avatar_updated_at": {
            "type": "integer",
            "nullable": true,
            "description": "Unix seconds of the current profile photo, for cache-busting; null without one."
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "description": "The time zone the caller runs on in this business.",
            "example": "America/Los_Angeles"
          },
          "time_zone_iana": {
            "type": "string",
            "nullable": true,
            "description": "The same zone as a canonical IANA id, for Intl and moment-timezone,\nwhich know no Rails name. Null only when the zone can't be resolved.\n",
            "example": "America/Los_Angeles"
          },
          "timezone_set": {
            "type": "boolean",
            "description": "Whether the caller chose a time zone themselves."
          },
          "employee_type": {
            "type": "string",
            "nullable": true,
            "example": "Full-Time"
          },
          "hire_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "description": "The job title this business shows for the caller.",
            "example": "Software Engineer"
          },
          "department": {
            "type": "object",
            "nullable": true,
            "description": "The caller's organizational department in this business, as its id and name; null when they have none here.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "example": "Engineering"
              }
            }
          },
          "language": {
            "type": "string",
            "description": "The language content reaches the caller in — their\nown setting, else their place's language, else the\ncompany default. Never null (Knowledge Layer P4).\n",
            "example": "en"
          },
          "language_set": {
            "type": "boolean",
            "description": "Whether the caller chose a language themselves;\n`false` is the profile's \"Not set\" (`language`\nthen follows their place or the company).\n"
          },
          "preferences": {
            "type": "object",
            "description": "The caller's stored preferences."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "User's permissions"
          },
          "feature_flags": {
            "type": "object",
            "description": "Feature flags for the user"
          },
          "custom_fields": {
            "type": "array",
            "description": "The caller's answers to the organization's own profile fields\n(People › Profile Fields) — the ones their profile shows them, in\nthe profile's order. Empty while custom fields are off.\n",
            "items": {
              "$ref": "#/components/schemas/ProfileCustomField"
            }
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProfileCustomField": {
        "description": "One of the organization's own profile fields (People › Profile Fields)\non a person, as the caller may see it there: an administrator sees every\nfield; the person, their manager and coworkers see the fields the\nfield's visibility gives them. Section headings carry no answer and are\nnot listed.\n",
        "type": "object",
        "required": [
          "field_name",
          "label",
          "field_type",
          "category",
          "value",
          "display_value",
          "required",
          "editable"
        ],
        "properties": {
          "field_name": {
            "type": "string",
            "example": "tshirt_size"
          },
          "label": {
            "type": "string",
            "example": "T-shirt size"
          },
          "field_type": {
            "type": "string",
            "description": "One of text, textarea, number, email, phone, url, select, multiselect, radio, checkbox, date, datetime, time, file, currency, rating.",
            "example": "select"
          },
          "category": {
            "type": "string",
            "description": "The profile section the field sits in.",
            "example": "additional_info"
          },
          "value": {
            "nullable": true,
            "description": "The stored answer (a string, or an array for a multiselect); null when unanswered.",
            "example": "L"
          },
          "display_value": {
            "type": "string",
            "nullable": true,
            "description": "The answer as the profile shows it (a date formatted, a checkbox Yes/No, a rating as stars); null when unanswered.",
            "example": "L"
          },
          "required": {
            "type": "boolean"
          },
          "editable": {
            "type": "boolean",
            "description": "Whether the caller may change this answer."
          }
        }
      },
      "UserDetail": {
        "description": "`GET /users/{id}` and `GET /users/by_email/{email}` — the list row\n(`User`) plus the HRIS detail below. `manager` here also carries\n`job_title`.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/User"
          },
          {
            "type": "object",
            "properties": {
              "direct_reports_count": {
                "type": "integer"
              },
              "skills_count": {
                "type": "integer"
              },
              "certifications_count": {
                "type": "integer"
              },
              "organizational_department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "legal_entity": {
                "type": "object",
                "nullable": true,
                "description": "The employing legal entity (Knowledge Layer X5): the person's own\noverride, else the nearest one set on their location or a place\nabove it. Null when none applies.\n",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "example": "Acme Retail GmbH"
                  },
                  "country_code": {
                    "type": "string",
                    "nullable": true,
                    "description": "ISO 3166-1 alpha-2, when the entity names one.",
                    "example": "DE"
                  }
                }
              },
              "flsa_status": {
                "type": "string",
                "nullable": true
              },
              "worker_classification": {
                "type": "string",
                "nullable": true
              },
              "time_zone": {
                "type": "string",
                "nullable": true
              },
              "language": {
                "type": "string",
                "nullable": true,
                "description": "The person's stored language SETTING — null means \"Not set\" (they\nfollow their place's language, else the company default). A\nprofile field, not the resolved language.\n"
              },
              "external_employee_id": {
                "type": "string",
                "nullable": true
              },
              "custom_fields": {
                "type": "array",
                "description": "The organization's own profile fields on this person that the\ncaller may see there. Empty while custom fields are off, and for\na token bound to no person.\n",
                "items": {
                  "$ref": "#/components/schemas/ProfileCustomField"
                }
              }
            }
          }
        ]
      },
      "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
          },
          "logo_background_color": {
            "type": "string",
            "nullable": true,
            "description": "HEX colour behind the logo on the sign-in screen (`GET /auth/login_config` only), composited over white; null when none is set or the backing is not a plain colour"
          },
          "login_message": {
            "type": "string",
            "nullable": true,
            "description": "The organization's sign-in message (help line), plain text with line breaks (`GET /auth/login_config` only); null when none is set"
          },
          "branding": {
            "type": "object",
            "nullable": true,
            "description": "Branding color configuration for mobile theming (HEX format). primary_button_color, status_success_color and status_error_color are sent readable under white text, as the phone web paints them.",
            "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
          },
          "managed_by": {
            "type": "string",
            "nullable": true,
            "example": "UKG",
            "description": "The workforce management system of record that owns this shift (\"Managed in UKG\"), or null. A managed shift is read-only here: coverage requests and edits answer 422 `externally_managed`."
          },
          "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"
              },
              "lister_status": {
                "type": "string",
                "enum": [
                  "open",
                  "filled",
                  "traded",
                  "closed",
                  "cancelled"
                ],
                "description": "Present only when the listing is the caller's own. The status as its lister reads it: listing_status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
                "example": "traded"
              },
              "lister_status_label": {
                "type": "string",
                "description": "Present only when the listing is the caller's own. The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
                "example": "Traded"
              },
              "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": {
          "managed_by": {
            "type": "string",
            "nullable": true,
            "example": "UKG",
            "description": "The workforce management system of record whose punch this is, or null. A managed punch (and its breaks) is read-only here."
          },
          "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
          },
          "review_reason_label": {
            "type": "string",
            "nullable": true,
            "description": "The words people see for `review_reason` (\"Missing-punch request\" for `missing_punch_request`); null when there is no reason."
          },
          "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 (false while their own request on it waits)"
          },
          "pending_request": {
            "type": "object",
            "nullable": true,
            "description": "The current user's own pickup request on this listing's shift that is still waiting for approval; withdraw it with POST /api/v1/shift_marketplace/listings/{id}/withdraw_claim",
            "properties": {
              "id": {
                "type": "integer"
              },
              "requested_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "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"
          },
          "lister_status": {
            "type": "string",
            "enum": [
              "open",
              "filled",
              "traded",
              "closed",
              "cancelled"
            ],
            "description": "Owner rows only (is_owner true). The status as its lister reads it — status, except a listing filled by an accepted trade is traded (filled = picked up). Switch on this key, never on lister_status_label.",
            "example": "traded"
          },
          "lister_status_label": {
            "type": "string",
            "description": "Owner rows only (is_owner true). The word the web and phone pages show the lister (Available, Covered, Traded, Closed, Cancelled), in the language the client asks for (the locale param, else Accept-Language, else the person's own language).",
            "example": "Traded"
          },
          "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"
          },
          "status_label": {
            "type": "string",
            "description": "The status as the app shows it. An application the app closed (its listing filled, closed or cancelled, or the offered shift gone) has status rejected and reads \"No Longer Available\".",
            "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"
          },
          "cancellation_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the app withdrew a cancelled offer (for example, the shift was filled or removed). Null when the offer is not cancelled, or when its sender cancelled it.\n"
          },
          "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"
          }
        }
      },
      "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": "The app's place in the business's app order (1 = first), the order the list is returned in",
            "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"
              }
            }
          },
          "nav": {
            "type": "object",
            "description": "The app as navigation shows it — the short word and the glyph the\nweb rail and the phone web's tab bar draw (the app's manifest:\nSidebarHelper#app_rail_label / #sidebar_app_icon). A tab bar draws\nthese; `name` and `icon` stay the app grid's.\n",
            "properties": {
              "label": {
                "type": "string",
                "example": "Comms"
              },
              "icon": {
                "type": "string",
                "description": "Font Awesome class.",
                "example": "fas fa-bullhorn"
              },
              "icon_type": {
                "type": "string",
                "enum": [
                  "icon_class"
                ],
                "description": "Always `icon_class` (MobileViewSupport::API_ICON_TYPES).",
                "example": "icon_class"
              }
            }
          },
          "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": "The app's place in the list as returned (1 = first) — the business's app order, after the viewer's pinned apps in the navigation format",
            "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); Safety Hub emits\n`safety_hub_*` keys and Service Desk `service_desk_*` keys\n(`service_desk_my_requests`, `service_desk_access_requests`,\n`service_desk_knowledge_base`, `service_desk_agent_dashboard`,\n`service_desk_tickets`, `service_desk_more`, plus\n`service_desk_problems` / `service_desk_changes` on `actions` or\npromoted to the top level) — namespaced because the native client\ndispatches every app's keys through one shared switch. Apps whose\nnavigation comes from the generic sidebar builder use that app's own\ntab keys.\n",
            "example": "review_queue"
          },
          "title": {
            "type": "string",
            "description": "Display label — never an identifier; branch on `key`. Service Desk's\ntitles are in the language the client asks for (`locale`, else\n`Accept-Language`, else the caller's own language); other apps'\ntitles are worded as the web sidebar shows them, in English today.\n",
            "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, apps built from the\nweb sidebar return the validated `/m/...` equivalent — a tab whose\ndesktop path has no real `/m/` route is dropped from the array\nrather than returned with a dead link. Apps whose mobile client\nrenders their tabs natively by `key` (Safety Hub, Service Desk)\nreturn the desktop path to mobile callers too, as the webview\nfallback. `null` on a `native: true` item; ABSENT on an item that\nonly groups `actions` (Service Desk's `service_desk_more`).\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": {
                "key": {
                  "type": "string",
                  "description": "Stable machine key for the action, safe to switch on — present\nonly where the builder emits one. Service Desk's\n`service_desk_more` carries `service_desk_problems`,\n`service_desk_changes` and `service_desk_access_requests` here, so the\nclient can open each tile of its More sheet natively.\n",
                  "example": "service_desk_problems"
                },
                "title": {
                  "type": "string",
                  "example": "New idea"
                },
                "url": {
                  "type": "string",
                  "description": "Path to open, following the same rule as the parent item's `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. **Print `formatted_date` and `formatted_time`.** Everything else in the time block is raw material for sorting, arithmetic and calendar entries.\n\nAn instructor-led session happens in a room, or a webinar scheduled by a host, in a specific PLACE — so every time here is anchored to the VENUE's zone, never the viewer's, and the web renders it the same way. A learner in another country sees the time the class actually starts, not their own clock.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 90
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Morning cohort"
          },
          "session_type": {
            "type": "string",
            "enum": [
              "classroom",
              "webinar",
              "hybrid"
            ],
            "example": "classroom"
          },
          "formatted_date": {
            "type": "string",
            "nullable": true,
            "description": "The session's date, composed server-side in the VENUE's zone and ready to print — the same string the web session row renders. Pair it with `formatted_time`.",
            "example": "Thu, Aug 20"
          },
          "formatted_time": {
            "type": "string",
            "nullable": true,
            "description": "The session's time range WITH its zone abbreviation, composed server-side in the VENUE's zone and ready to print — the same string the web session row renders.\n\nPRINT THIS rather than formatting `starts_at` yourself. The natural mistake (`new Date(starts_at)` and a local format) renders in the DEVICE's zone and shows a learner in another country a time the session does not start at. There is nothing to get wrong here.",
            "example": "11:00 AM – 3:00 PM EDT"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The instant, carrying the VENUE's UTC offset (not `Z`, and not the caller's offset). For sorting, countdowns and calendar entries; for display use `formatted_time`.\n\nUntil 2026-09-16 this was serialized from the raw column, which renders in the ambient request zone — so it was documented as UTC and was in fact the CALLER's offset, differing per viewer while `timezone` beside it named the venue. The instant was always correct, so any client parsing it as an instant is unaffected; only naive string handling changes, and it changes from an unstable caller clock to the stable venue clock.",
            "example": "2026-08-20T11:00:00-04:00"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "As `starts_at` — the VENUE's offset.",
            "example": "2026-08-20T15:00:00-04:00"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "IANA identifier for the VENUE's zone (`America/New_York`, `Etc/UTC`), resolvable by `TimeZone(identifier:)`, `ZoneId.of` and `Intl.DateTimeFormat`. NOT needed for display — `formatted_time` already carries the zone. Needed when you want the zone's IDENTITY rather than one instant's offset: a calendar entry's `TZID`, or arithmetic that crosses a daylight-saving boundary. An offset cannot substitute, because the mapping is many-to-one — `-05:00` is Chicago in summer, New York in winter and Bogota all year.\n\nNormalized on the way out as of 2026-09-16. The stored value is an ActiveSupport zone NAME (the authoring form writes those deliberately), so this field previously returned strings like `International Date Line West` that no client zone API resolves — 10 of 14 sessions on one tenant.",
            "example": "America/New_York"
          },
          "timezone_label": {
            "type": "string",
            "nullable": true,
            "description": "Short zone abbr, or UTC±offset. NOW REDUNDANT — the same text closes `formatted_time`. Retained for compatibility; do not build on it. Useful only if you render the abbreviation in different styling from the time.",
            "example": "EDT"
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true,
            "description": "Derived from the two instants, so you need not subtract. Zone-independent.",
            "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,
                "description": "When registration closes — the session's own start when no explicit deadline was set. Carries the VENUE's UTC offset, like `starts_at`.",
                "example": "2026-08-19T11:00:00-04:00"
              }
            }
          },
          "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. `locked` — the request's API token lacks write:training, so the row offers none of join_waitlist / switch / register (each a POST asking it); draw no button. A signed-in session never gets it.",
            "enum": [
              "registered",
              "waitlisted",
              "closed",
              "join_waitlist",
              "switch",
              "register",
              "locked"
            ],
            "example": "register"
          }
        }
      },
      "TrainingSeriesRow": {
        "type": "object",
        "description": "One recurring instructor-led class on a course, as the caller sees it — the web's \"Recurring class\" card. Its dates are still listed one by one in `sessions`; this is the one entry to join or leave (ISS-20261102-911-B4E0DF).",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The series id the membership endpoints take.",
            "example": 12
          },
          "title": {
            "type": "string",
            "example": "Weekly safety huddle"
          },
          "schedule_summary": {
            "type": "string",
            "description": "The repeat rule in words.",
            "example": "Every week on Tuesday, 12 times"
          },
          "next_session": {
            "type": "object",
            "nullable": true,
            "description": "The next date still to run, with the same venue-zone time block every session payload carries (print `formatted_date` / `formatted_time`). Null when no date is left.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 90
              },
              "formatted_date": {
                "type": "string",
                "nullable": true,
                "example": "Tue, Nov 4"
              },
              "formatted_time": {
                "type": "string",
                "nullable": true,
                "example": "9:00 AM – 10:00 AM EST"
              },
              "starts_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "ends_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "timezone": {
                "type": "string",
                "nullable": true,
                "example": "America/New_York"
              },
              "timezone_label": {
                "type": "string",
                "nullable": true,
                "example": "EST"
              },
              "duration_minutes": {
                "type": "integer",
                "nullable": true,
                "example": 60
              }
            }
          },
          "dates_left": {
            "type": "integer",
            "description": "Dates still to run.",
            "example": 8
          },
          "member": {
            "type": "boolean",
            "description": "The caller is in this class."
          },
          "next_seat_id": {
            "type": "integer",
            "nullable": true,
            "description": "The caller's seat (registration id) on `next_session`, or null. Offer \"Can't make the next one\" only when it is set; it is `DELETE /training/sessions/{next_session.id}/registration`, which keeps the caller in the class."
          }
        }
      },
      "TrainingSeriesMembershipResponse": {
        "type": "object",
        "properties": {
          "series": {
            "$ref": "#/components/schemas/TrainingSeriesRow"
          },
          "message": {
            "type": "string",
            "description": "The sentence to show — the web's.",
            "example": "You're in Weekly safety huddle. It's on your calendar as one series."
          },
          "waitlisted": {
            "type": "integer",
            "description": "Dates the class was full on, where the caller is on the waitlist.",
            "example": 0
          },
          "unread_notification_count": {
            "$ref": "#/components/schemas/UnreadNotificationCount"
          }
        }
      },
      "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",
              "practice"
            ],
            "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 lesson's time in minutes: the author's estimate, or — for a text or quiz lesson nobody timed — worked out from its text (about 200 words a minute, at least one) or its quiz (its time limit, else a minute a question an attempt is given). Null when there is neither. 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 · 5m read\", \"Video lesson · 0:48 watch\", \"PDF document · 10m\", \"SCORM module\". An estimate is written as Training writes every length (\"45m\", \"1h 40m\"), a measured video as its player's clock. 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,
            "description": "Whether the lesson counts toward completing the course — the course's progress rule over this outline. On a course with no required lesson every playable lesson counts, so every row is true; an `unavailable` lesson is always false."
          },
          "unavailable": {
            "type": "boolean",
            "example": false,
            "description": "True when the COURSE cannot play this lesson right now, for any learner (a partner licence ended, a scenario was removed, Practice Hub is off). It is not optional — show it as unavailable, with `unavailable_reason`; it is left out of the progress count until it plays again. A lesson only the caller cannot take (no partner seat left for them) is NOT unavailable here."
          },
          "unavailable_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "no_course",
              "unpublished",
              "no_license",
              "no_portal",
              "no_scenario",
              "app_disabled",
              "no_persona",
              null
            ],
            "example": null,
            "description": "Why the lesson is `unavailable`, else null. Learner-safe wording the web uses: no_course / unpublished / no_scenario → \"content removed\"; no_license → \"licence ended\"; no_portal → \"provider not connected\"; app_disabled → \"practice not enabled\"; no_persona → \"not ready yet\"."
          },
          "completed": {
            "type": "boolean",
            "example": false
          },
          "current": {
            "type": "boolean",
            "description": "The lesson the CTA resumes into: the first lesson not yet completed, else the first. At most one row carries this. While the course is in progress it is always the same lesson as the detail's `cta.lesson_id`, so a client can open the player from either. On a finished course `cta` names no lesson and this row still carries the flag (an optional lesson left undone, else the first lesson) — it is not work left to do; the detail's `secondary_cta.lesson_id`, when set, is where Review course opens. 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. A locked row is the outline only: `web_view_url` is null, and the lesson's content endpoint refuses the caller.",
            "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, and on a `locked` row (the caller holds no enrollment, and the page would refuse them).\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. Asking, answering and upvoting carry no per-owner gate on the web either, so for a signed-in caller who got a 200 they are true; each is a POST asking write:training, so an API token without it gets false for all of them. (Upvoting a row of your own is still refused per row, 422.)",
            "properties": {
              "can_ask": {
                "type": "boolean",
                "example": true
              },
              "can_answer": {
                "type": "boolean",
                "example": true
              },
              "can_vote": {
                "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
          },
          "overdue_paths": {
            "type": "integer",
            "description": "The team's current learning paths past their due date — counted apart from `overdue`, which is course enrollments only. The Overdue tab lists the first page of them in `overdue_paths`.",
            "example": 2
          }
        }
      },
      "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
          }
        }
      },
      "TrainingChecklistPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          }
        }
      },
      "TrainingChecklistSignoff": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "item_key": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "met",
              "unmet",
              "not_applicable"
            ]
          },
          "result_label": {
            "type": "string",
            "example": "Met"
          },
          "passing": {
            "type": "boolean",
            "description": "Met and N/A count toward completing the lesson."
          },
          "validation_method": {
            "type": "string",
            "nullable": true
          },
          "comment": {
            "type": "string",
            "nullable": true
          },
          "signer_name": {
            "type": "string"
          },
          "signer_title": {
            "type": "string",
            "nullable": true
          },
          "signed_at": {
            "type": "string",
            "format": "date-time"
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "The caller may withdraw it (POST …/withdraw) now — the record is still open and the token holds `destructive:training`."
          }
        }
      },
      "TrainingChecklistAttempt": {
        "type": "object",
        "properties": {
          "enrollment_id": {
            "type": "integer"
          },
          "lesson": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              }
            }
          },
          "course": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "learner": {
            "$ref": "#/components/schemas/TrainingChecklistPerson"
          },
          "progress": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "passing": {
                "type": "integer"
              },
              "signed": {
                "type": "integer"
              },
              "complete": {
                "type": "boolean"
              }
            }
          },
          "can_sign": {
            "type": "boolean",
            "description": "The caller signs this learner's items now, and the token may write."
          },
          "sign_refusal": {
            "type": "string",
            "nullable": true,
            "description": "Why nothing can be signed now; null when it can."
          },
          "validation_methods": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "number": {
                  "type": "integer",
                  "nullable": true
                },
                "section": {
                  "type": "string",
                  "nullable": true
                },
                "text": {
                  "type": "string"
                },
                "guidance": {
                  "type": "string",
                  "nullable": true
                },
                "results": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "value": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      }
                    }
                  }
                },
                "comment_required": {
                  "type": "boolean"
                },
                "can_sign": {
                  "type": "boolean"
                },
                "signoff": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TrainingChecklistSignoff"
                    }
                  ],
                  "nullable": true
                },
                "history_count": {
                  "type": "integer",
                  "description": "Earlier sign-offs of this item (superseded or withdrawn), kept in its history."
                }
              }
            }
          }
        }
      },
      "TrainingTeamPathEnrollmentRow": {
        "type": "object",
        "description": "One learning-path row on My Team: the dashboard's `overdue_paths` (with `learner`) and the drill-in's `paths`. `can_remind` is the web row's own test — still owed, and dated — on a token holding `write:training`, and POST /training/my_team/{id}/remind with `path_enrollment_id` sends it.",
        "properties": {
          "path_enrollment_id": {
            "type": "integer"
          },
          "learning_path_id": {
            "type": "integer",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "enrolled",
              "in_progress",
              "completed",
              "cancelled"
            ]
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "overdue": {
            "type": "boolean"
          },
          "days_overdue": {
            "type": "integer",
            "nullable": true,
            "description": "Null unless overdue."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "can_remind": {
            "type": "boolean"
          },
          "learner": {
            "type": "object",
            "nullable": true,
            "description": "Present on the dashboard's rows only.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "initials": {
                "type": "string",
                "nullable": true
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "avatar_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              }
            }
          }
        }
      },
      "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"
          },
          "course_archived": {
            "type": "boolean",
            "description": "true when the row's course has been archived. The member drill-in's `open` list keeps such a row so it can be removed, but the learner no longer sees it and `bucket_counts` leaves it out; the web and phone drill-ins label it \"Course archived\" in place of its status.",
            "example": false
          },
          "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",
          "overdue_paths",
          "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 `direct_reports` unless they ask for `whole_line` (their direct reports and everyone below them) and have a line below their reports.",
                "enum": [
                  "direct_reports",
                  "all_employees",
                  "whole_line"
                ],
                "example": "all_employees"
              },
              "available": {
                "type": "array",
                "description": "What THIS caller may request via `?scope=`. `direct_reports` and `all_employees` for a Training admin, `[\"direct_reports\"]` alone for a people-manager, plus `whole_line` for anyone whose reports have reports — the role is a ceiling, so a manager requesting `all_employees` is still served their own team (see `reach_label`).",
                "items": {
                  "type": "string",
                  "enum": [
                    "direct_reports",
                    "all_employees",
                    "whole_line"
                  ]
                },
                "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
              },
              "reach_label": {
                "type": "string",
                "nullable": true,
                "description": "Set when the `direct_reports` rows also hold the people of the places this caller looks after — a Training role limited to specific places, or a location contact role the business chose in Training settings: the people based at those places (or beneath them) and anyone posted there today. The words name the places (\"Platform B\"); null for everyone else. Show it beside \"Team\" so the roster is not read as direct reports alone.",
                "example": "Platform B"
              },
              "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"
          },
          "actions": {
            "$ref": "#/components/schemas/TrainingManagerBrief"
          },
          "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,
                  "description": "On `overdue`: the overdue COURSE enrollments."
                },
                "path_count": {
                  "type": "integer",
                  "example": 2,
                  "description": "On `overdue` only — the team's overdue learning paths, listed in `overdue_paths`."
                }
              }
            }
          },
          "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"
            }
          },
          "overdue_paths": {
            "type": "array",
            "description": "The team's overdue LEARNING PATHS — the web Overdue page's second list — soonest-due first, as many as a page shows (`stats.overdue_paths` is their total; open a member's drill-in for the rest). Each row names its learner. Populated only when `active_tab` is `overdue`.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamPathEnrollmentRow"
            }
          },
          "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`."
          }
        }
      },
      "TrainingManagerBrief": {
        "type": "object",
        "description": "The manager's action brief for the same team as `scope` — the work waiting on them, counted before pagination (Training::ManagerBrief). Without `brief_kind`, `rows` is the top five actions across every kind the caller has not hidden, and `hidden` the ones passed over to reach them. With `brief_kind`, `rows` is one 20-row page of that kind (`brief_page`), each row carrying its own `hidden` flag.",
        "required": [
          "counts",
          "total",
          "kind",
          "page",
          "has_more",
          "rows",
          "hidden"
        ],
        "properties": {
          "counts": {
            "type": "object",
            "description": "Open actions per kind, for the whole team.",
            "properties": {
              "signoffs": {
                "type": "integer",
                "description": "Checklists to sign.",
                "example": 2
              },
              "follow_up": {
                "type": "integer",
                "description": "Assessment follow-up.",
                "example": 1
              },
              "renewals": {
                "type": "integer",
                "description": "Credentials to renew.",
                "example": 0
              },
              "overdue": {
                "type": "integer",
                "description": "Overdue training.",
                "example": 4
              }
            }
          },
          "total": {
            "type": "integer",
            "description": "The sum of `counts`.",
            "example": 7
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "description": "The `brief_kind` applied; null for the top-five view (an unrecognised `brief_kind` is ignored).",
            "enum": [
              "signoffs",
              "follow_up",
              "renewals",
              "overdue",
              null
            ],
            "example": null
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "example": 1
          },
          "has_more": {
            "type": "boolean",
            "nullable": true,
            "description": "With a `kind`, true when another 20-row page follows; null in the top-five view.",
            "example": null
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrainingManagerBriefRow"
            }
          },
          "hidden": {
            "type": "array",
            "description": "Top-five view only — the actions the caller hid that were passed over (always `hidden: true`); empty with a `kind`.",
            "items": {
              "$ref": "#/components/schemas/TrainingManagerBriefRow"
            }
          }
        }
      },
      "TrainingManagerBriefRow": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "learner",
          "user_id",
          "source_id",
          "url",
          "action_label",
          "signature",
          "hidden"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable id of the action (`<kind>_<source_id>`, plus the lesson id on a sign-off).",
            "example": "overdue_812"
          },
          "kind": {
            "type": "string",
            "enum": [
              "signoffs",
              "follow_up",
              "renewals",
              "overdue"
            ],
            "example": "overdue"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "The course, quiz or credential the action is about.",
            "example": "Forklift Safety"
          },
          "learner": {
            "type": "string",
            "description": "The learner's name (\"Unnamed user\" when they have none).",
            "example": "Dana Ruiz"
          },
          "user_id": {
            "type": "integer",
            "example": 77
          },
          "source_id": {
            "type": "integer",
            "description": "The enrollment, assessment attempt or certification the action is about.",
            "example": 812
          },
          "url": {
            "type": "string",
            "description": "The web page that takes the action (a path on the tenant host); it opens behind that page's own permission check.",
            "example": "/apps/training/manager/team/77"
          },
          "action_label": {
            "type": "string",
            "enum": [
              "Open learner",
              "Review checklist",
              "Review follow-up"
            ],
            "example": "Open learner"
          },
          "signature": {
            "type": "string",
            "description": "Changes when the action changes; a hidden action comes back once its signature no longer matches."
          },
          "hidden": {
            "type": "boolean",
            "description": "True when the caller hid this action and it has not changed since.",
            "example": false
          }
        }
      },
      "TrainingMyTeamMember": {
        "type": "object",
        "required": [
          "member",
          "departments",
          "counts",
          "bucket_counts",
          "open",
          "completed",
          "paths",
          "certificates"
        ],
        "properties": {
          "member": {
            "$ref": "#/components/schemas/TrainingTeamMemberRow"
          },
          "departments": {
            "type": "array",
            "description": "Every department the learner belongs to in this organization, in the order the web and phone drill-ins list them under the job title. Empty when they belong to none, or where the organization's field settings keep Department from the caller.",
            "items": {
              "type": "string"
            },
            "example": [
              "Design",
              "Marketing"
            ]
          },
          "counts": {
            "type": "object",
            "description": "LEGACY, may overlap — unchanged for shipped apps. By raw status: `assigned` is not begun, `in_progress` started; `overdue` counts outstanding rows past their due date that are ALSO in `assigned` or `in_progress`. New clients read `bucket_counts`.",
            "properties": {
              "assigned": {
                "type": "integer",
                "description": "Legacy — not begun (raw status enrolled); may overlap overdue.",
                "example": 2
              },
              "in_progress": {
                "type": "integer",
                "description": "Legacy — may overlap overdue.",
                "example": 3
              },
              "completed": {
                "type": "integer",
                "example": 5
              },
              "overdue": {
                "type": "integer",
                "example": 1
              }
            }
          },
          "bucket_counts": {
            "type": "object",
            "description": "The PARTITION of what the learner has outstanding, as the web and phone drill-ins and the learner's own My Learning show it (Training::LearningStatus): each open course or path is in exactly one bucket, overdue first.",
            "required": [
              "not_started",
              "in_progress",
              "overdue"
            ],
            "properties": {
              "not_started": {
                "type": "integer",
                "example": 2
              },
              "in_progress": {
                "type": "integer",
                "example": 2
              },
              "overdue": {
                "type": "integer",
                "example": 1
              }
            }
          },
          "open": {
            "type": "array",
            "description": "Everything outstanding, newest first. `learner` is omitted (the header names them). A row whose course has since been archived is listed too, with `course_archived: true`, so it can be removed; it is not counted in `bucket_counts`.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "completed": {
            "type": "array",
            "description": "The completed history, newest first.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamEnrollmentRow"
            }
          },
          "paths": {
            "type": "array",
            "description": "Their learning paths (not cancelled), newest first — already counted in `counts` / `bucket_counts`. Remind one with `path_enrollment_id`.",
            "items": {
              "$ref": "#/components/schemas/TrainingTeamPathEnrollmentRow"
            }
          },
          "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_vote": {
                "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
          }
        }
      },
      "TrainingVersionBrief": {
        "type": "object",
        "nullable": true,
        "description": "Factual changes since the caller's last actually completed course edition. Null without a prior completion. Unknown historical lineage returns known false. This never waives retraining.",
        "required": [
          "known",
          "changes"
        ],
        "properties": {
          "known": {
            "type": "boolean"
          },
          "previous_version": {
            "type": "integer"
          },
          "current_version": {
            "type": "integer"
          },
          "previous_version_id": {
            "type": "integer"
          },
          "current_version_id": {
            "type": "integer"
          },
          "total": {
            "type": "integer",
            "description": "Total changes before the 100-row response limit."
          },
          "capped": {
            "type": "boolean"
          },
          "reviewed_briefing": {
            "type": "object",
            "nullable": true,
            "description": "Human-reviewed meaning, only while both exact edition digests still match.",
            "properties": {
              "summary": {
                "type": "string"
              },
              "reviewed_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "changes": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "added",
                    "removed",
                    "changed",
                    "assessment_review"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "previous_lesson_id": {
                  "type": "integer",
                  "nullable": true
                },
                "current_lesson_id": {
                  "type": "integer",
                  "nullable": true
                },
                "media_review_needed": {
                  "type": "boolean"
                },
                "before": {
                  "type": "string",
                  "nullable": true,
                  "description": "Plain text excerpt for native text lessons only."
                },
                "after": {
                  "type": "string",
                  "nullable": true,
                  "description": "Plain text excerpt for native text lessons only."
                }
              }
            }
          }
        }
      },
      "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",
              "open_course",
              "view_certificate",
              "enroll",
              "buy",
              "choose_session",
              "view_session",
              "start_path",
              "continue_step",
              "view_steps",
              "locked"
            ],
            "example": "resume"
          },
          "course_id": {
            "type": "integer",
            "description": "open_course only — the selected approved language course."
          },
          "enrollment_id": {
            "type": "integer",
            "description": "open_course only — the actual language enrollment."
          },
          "fulfilled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "open_course only — when that course satisfied the original assignment."
          },
          "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. `read_only_token` — the request's API token lacks write:training, which the enroll / register POST behind `enroll`, `choose_session` or `start_path` asks; a signed-in session never gets it. All are values the detail serializers send; listed in full because a closed enum in a generated client fails the whole response on an unknown one.",
            "enum": [
              "admin_enrolls",
              "purchase_required",
              "read_only_token"
            ],
            "example": "admin_enrolls"
          }
        }
      },
      "UnreadNotificationCount": {
        "type": "integer",
        "nullable": true,
        "description": "**Envelope 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* A count of the Inbox, so `null` for a token without\n  `read:notifications` (the key stays; the native app's sign-in tokens\n  carry the scope).\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
      },
      "PlatformHttpMeta": {
        "type": "object",
        "description": "The `_meta` block `render_with_piggyback` adds through\n`HttpEnhancementService`. `http.caching.etag` is the same value as the\nresponse's `ETag` header (a weak `W/\"…\"` tag fingerprinting the body\nwithout `_meta`); send it back as `If-None-Match` on the next GET of the\nsame URL to get `304 Not Modified` while nothing changed.\n",
        "properties": {
          "http": {
            "type": "object",
            "properties": {
              "caching": {
                "type": "object",
                "properties": {
                  "etag": {
                    "type": "string",
                    "example": "W/\"3f2a9c1e0b7d4e6f8a1b2c3d4e5f6a7b\""
                  },
                  "max_age": {
                    "type": "integer"
                  },
                  "private": {
                    "type": "boolean",
                    "description": "True for a signed-in caller; the response then carries `Cache-Control: private, max-age=0, must-revalidate`."
                  },
                  "revalidate": {
                    "type": "boolean"
                  },
                  "vary": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "cacheable": {
                    "type": "boolean"
                  }
                }
              },
              "navigation": {
                "type": "object"
              },
              "performance": {
                "type": "object"
              },
              "client_hints": {
                "type": "object"
              }
            }
          }
        }
      },
      "CollectionPaginationMeta": {
        "description": "The `meta` block of a paginated list rendered through\n`render_collection_with_piggyback`\n(`Api::V1::BaseController#build_pagination_meta`): the four\nPaginationMeta values plus the two page-direction flags. The same four\nvalues are also returned as the `X-Total-Count`, `X-Total-Pages`,\n`X-Current-Page` and `X-Per-Page` response headers.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/PaginationMeta"
          },
          {
            "type": "object",
            "properties": {
              "has_next_page": {
                "type": "boolean",
                "example": true
              },
              "has_prev_page": {
                "type": "boolean",
                "example": false
              }
            }
          }
        ]
      },
      "CollectionEnvelope": {
        "type": "object",
        "description": "The keys every list endpoint that renders through\n`Api::V1::BaseController#render_collection_with_piggyback` adds beside\nits own `items` (and, when paginated, `meta`). The same response\ncarries an `ETag` header with\n`Cache-Control: private, max-age=0, must-revalidate`, and a GET whose\n`If-None-Match` matches that ETag is answered `304 Not Modified` with\nan empty body (see `components/responses/NotModified`).\n",
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Total rows across all pages; same value as meta.total_count when the list is paginated.",
            "example": 42
          },
          "unread_notification_count": {
            "$ref": "#/components/schemas/UnreadNotificationCount"
          },
          "_meta": {
            "$ref": "#/components/schemas/PlatformHttpMeta"
          }
        }
      },
      "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
              }
            }
          },
          "theme": {
            "type": "object",
            "description": "The colour picked in the form's Design modal (a custom palette's Brand primary, or the chosen preset's). Both values are `null` when the form follows the business theme — keep the business branding.",
            "properties": {
              "brand_primary": {
                "type": "string",
                "nullable": true,
                "description": "The picked brand colour as a",
                "example": "#f5c518"
              },
              "brand_primary_fill": {
                "type": "string",
                "nullable": true,
                "description": "The same colour moved until white button text clears it (WCAG AA, 4.5:1) — the fill the phone web fill page paints behind its buttons, links and checked controls. `null` when the form follows the business theme.",
                "example": "#7d6302"
              }
            }
          },
          "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. An identity or account number held sealed (an SSN, tax id, passport, bank routing / account or card number) is never sent back: its value is \"\" and `on_file` carries the mask."
          },
          "on_file": {
            "type": "string",
            "description": "Present only when this field's saved value is an identity or account number held encrypted on the submission: its mask, e.g. \"****6789\". The input starts empty and is not required (`required` is false); an empty answer keeps the number on file, a new one replaces it.",
            "example": "****6789"
          }
        },
        "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,
            "description": "The next compensation review date your organization has set; null when none is set.",
            "example": "2024-12-01"
          },
          "last_updated": {
            "type": "object",
            "description": "The most recent compensation change on record; both fields are null when there is none.",
            "properties": {
              "date": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the change was recorded.",
                "example": "2024-01-15T10:30:00Z"
              },
              "by": {
                "type": "string",
                "nullable": true,
                "description": "Who made the change, or \"System\" for a change recorded with no author (an automated step increase or import).",
                "example": "Jane Smith"
              }
            }
          }
        }
      },
      "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",
            "description": "Not tracked yet — both fields are always null.",
            "properties": {
              "has_recent_review": {
                "type": "boolean",
                "nullable": true,
                "description": "Not tracked yet; always null.",
                "example": null
              },
              "merit_increase_eligible": {
                "type": "boolean",
                "nullable": true,
                "description": "Not tracked yet; always null.",
                "example": null
              }
            }
          }
        }
      },
      "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",
            "description": "Approved and applied to pay.",
            "example": 1
          },
          "not_applied": {
            "type": "integer",
            "description": "Approved, but the pay change could not be applied (see each request's not-applied reason).",
            "example": 0
          },
          "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,
                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title."
              }
            }
          },
          "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": {
            "type": "object",
            "description": "Which SMART criteria the template meets (GoalTemplate#smart_criteria_hash)",
            "properties": {
              "is_specific": {
                "type": "boolean"
              },
              "is_measurable": {
                "type": "boolean"
              },
              "is_achievable": {
                "type": "boolean"
              },
              "is_relevant": {
                "type": "boolean"
              },
              "is_time_bound": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "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, as Api::V1::Epms::BaseController #format_performance_review_response sends it. The fields under \"detail only\" are present on the single-review responses (show, create, update, submit, approve), not on the list.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 234
          },
          "title": {
            "type": "string",
            "example": "Q1 2026 Performance Review"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "in_progress",
              "completed",
              "approved",
              "archived"
            ],
            "example": "in_progress"
          },
          "display_status": {
            "type": "string",
            "description": "The status as shown — `overdue` for a pending review past its due date, else `status`.",
            "enum": [
              "created",
              "in_progress",
              "completed",
              "approved",
              "archived",
              "overdue"
            ],
            "example": "in_progress"
          },
          "status_color": {
            "type": "string",
            "description": "Bootstrap badge color variant for the display status",
            "enum": [
              "info",
              "primary",
              "warning",
              "success",
              "danger",
              "secondary"
            ],
            "example": "primary"
          },
          "display_status_label": {
            "type": "string",
            "description": "The display status in words, as the web list labels it.",
            "example": "In Progress"
          },
          "review_type": {
            "type": "string",
            "enum": [
              "annual",
              "biannual",
              "quarterly",
              "probationary",
              "ad_hoc"
            ],
            "example": "quarterly"
          },
          "review_period": {
            "type": "string",
            "description": "The review period in words.",
            "example": "Jan 2026 - Mar 2026"
          },
          "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",
            "nullable": true,
            "example": "2026-04-15"
          },
          "overall_rating": {
            "type": "number",
            "nullable": true,
            "description": "The numeric rating — null unless the caller may see the manager's work and the business shows numeric scores.",
            "example": 4.5
          },
          "overall_rating_display": {
            "type": "string",
            "nullable": true,
            "description": "The rating as the business displays it (a number, a label, or hidden); null when the caller may not see the manager's work.",
            "example": "4.5 / 5"
          },
          "rating_scale": {
            "type": "integer",
            "example": 5
          },
          "completion_percentage": {
            "type": "number",
            "example": 60
          },
          "is_overdue": {
            "type": "boolean",
            "example": false
          },
          "days_until_due": {
            "type": "integer",
            "nullable": true,
            "example": 12
          },
          "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"
          },
          "employee": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 456
              },
              "name": {
                "type": "string",
                "example": "John Doe"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "example": "john.doe@example.com"
              },
              "job_title": {
                "type": "string",
                "nullable": true,
                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                "example": "Software Engineer"
              }
            }
          },
          "reviewer": {
            "type": "object",
            "description": "The reviewer; absent when the review has none.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 789
              },
              "name": {
                "type": "string",
                "example": "Jane Manager"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "example": "jane.manager@example.com"
              }
            }
          },
          "manager_comments": {
            "type": "string",
            "nullable": true,
            "description": "Detail only. Null when the caller may not see the manager's work.",
            "example": "Great progress this quarter"
          },
          "employee_self_assessment": {
            "type": "string",
            "nullable": true,
            "description": "Detail only."
          },
          "goals_count": {
            "type": "integer",
            "description": "Detail only."
          },
          "completed_goals_count": {
            "type": "integer",
            "description": "Detail only."
          },
          "goals_completion_percentage": {
            "type": "number",
            "description": "Detail only."
          },
          "can_edit": {
            "type": "boolean",
            "description": "Detail only."
          },
          "can_submit": {
            "type": "boolean",
            "description": "Detail only."
          },
          "can_approve": {
            "type": "boolean",
            "description": "Detail only."
          },
          "self_assessment_required": {
            "type": "boolean",
            "description": "Detail only."
          },
          "employee_submitted_self_assessment": {
            "type": "boolean",
            "description": "Detail only."
          },
          "meeting_completed": {
            "type": "boolean",
            "description": "Detail only."
          }
        }
      },
      "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,
                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                "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,
                "description": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own title.",
                "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": "The job title this business shows for the person: their membership's title (set by the profile's Job Title select), else the profile's own 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"
          },
          "display_state": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "under_review",
              "approved",
              "rejected",
              "pending_completion",
              "changes_requested",
              "completed"
            ],
            "description": "What the Forms pages call it. `completed` is a sent submission on a form that\nneeds no approval (its `status` stays `submitted`); otherwise the same as `status`.\n",
            "example": "completed"
          },
          "status_label": {
            "type": "string",
            "description": "Human-readable label of `display_state`, as the pages' status badge prints it",
            "example": "Completed"
          },
          "completion_percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Share of the form's questions answered (optional ones left blank keep it under 100) — not progress toward Completed; the pages label it \"Answered\"",
            "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"
              },
              "display_state": {
                "type": "string",
                "enum": [
                  "draft",
                  "submitted",
                  "under_review",
                  "approved",
                  "rejected",
                  "pending_completion",
                  "changes_requested",
                  "completed"
                ],
                "description": "What the Forms pages call it. `completed` is a sent submission on a form that\nneeds no approval (its `status` stays `submitted`); otherwise the same as `status`.\n",
                "example": "completed"
              },
              "status_label": {
                "type": "string",
                "description": "Human-readable label of `display_state`, as the pages' status badge prints it",
                "example": "Completed"
              },
              "completion_percentage": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "Share of the form's questions answered (optional ones left blank keep it under 100) — not progress toward Completed; the pages label it \"Answered\"",
                "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"
            ]
          },
          {
            "$ref": "#/components/schemas/FormSubmissionDecisionCapabilities"
          },
          {
            "type": "object",
            "properties": {
              "can_approve": {
                "type": "boolean",
                "description": "May this caller approve it now — false after an intermediate workflow step they signed."
              },
              "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"
                  }
                }
              }
            }
          }
        ]
      },
      "FormSubmissionDecisionCapabilities": {
        "type": "object",
        "properties": {
          "can_reject": {
            "type": "boolean",
            "description": "May decide it while it awaits a decision, with a token carrying `destructive:forms`."
          },
          "can_return_fields": {
            "type": "boolean",
            "description": "May decide it and there is someone to return it to — not an anonymous\nsubmission, not a submitter who has left, and a customer's answers to a\nproject's form request only while the request is open to them.\n"
          },
          "return_fields_refusal": {
            "type": "string",
            "nullable": true,
            "description": "Why Return is off for someone who may decide it; null otherwise."
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "The submitter may withdraw it (`POST /form_submissions/{id}/withdraw`)."
          },
          "withdraw_refusal": {
            "type": "string",
            "nullable": true,
            "description": "Why its submitter can't withdraw it while it waits for review; null otherwise."
          },
          "can_reopen": {
            "type": "boolean",
            "description": "May reopen the approval for review (`POST /form_submissions/{id}/reopen`)."
          },
          "reopen_refusal": {
            "type": "string",
            "nullable": true,
            "description": "Why an approved submission can't be reopened, to someone who may decide it; null otherwise."
          },
          "can_take_back_returns": {
            "type": "boolean",
            "description": "May take back its return for correction (`POST /form_submissions/{id}/take_back_returns`)."
          },
          "take_back_returns_refusal": {
            "type": "string",
            "nullable": true,
            "description": "Why a returned submission's return can't be taken back, to someone who manages it; null otherwise."
          }
        }
      },
      "SubmissionTimelineEvent": {
        "type": "object",
        "description": "A single event on the submission status timeline",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "submitted",
              "approved",
              "changes_requested",
              "rejected",
              "withdrawn",
              "reopened_for_review",
              "returns_taken_back"
            ],
            "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",
            "nullable": true,
            "description": "When the event occurred; null for `withdrawn`, which keeps no time"
          },
          "by": {
            "type": "object",
            "nullable": true,
            "description": "Who performed the event (the reviewer; the sender for `withdrawn`); 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), or the reason given for a reopen or a return taken back; 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
              }
            }
          },
          "source": {
            "$ref": "#/components/schemas/ManagedSource"
          }
        },
        "required": [
          "leave_type_id",
          "leave_type",
          "year",
          "accrued",
          "used",
          "balance",
          "pending",
          "percentages"
        ]
      },
      "ManagedSource": {
        "type": "object",
        "nullable": true,
        "description": "Null for a record Workforce keeps itself. For a leave balance or timesheet mirrored from the workforce management system of record (UKG), it is read-only in Workforce and changed in that system.",
        "properties": {
          "managed_by": {
            "type": "string",
            "description": "The system's name as people see it (\"Managed in UKG\")",
            "example": "UKG"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the system last reported this record"
          },
          "stale": {
            "type": "boolean",
            "description": "True when that was too long ago to trust (36 hours) for a record the system still sends; a closed pay period's timesheet or a past year's balance is final, never stale"
          },
          "fix_url": {
            "type": "string",
            "nullable": true,
            "description": "The system's own address, where the record is corrected; null when not configured"
          },
          "read_only": {
            "type": "boolean",
            "example": true
          },
          "approved_not_reflected_hours": {
            "type": "number",
            "description": "Leave balances only. Hours of leave approved in Workforce, starting on or after as_of, that the system's balance does not show yet. The balance itself is the system's, unchanged; these hours are already counted against what is left to book.",
            "example": 16.0
          },
          "signed_off": {
            "type": "boolean",
            "description": "Timesheets only. Whether the system has signed off this pay period's timecard."
          },
          "signed_off_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Timesheets only. When the system signed it off, as first reported."
          }
        }
      },
      "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). Hour figures are strings with two decimals.",
            "properties": {
              "accrued": {
                "type": "number",
                "example": 15.0
              },
              "used": {
                "type": "number",
                "example": 5.0
              },
              "balance": {
                "type": "number",
                "example": 10.0
              },
              "balance_hours": {
                "type": "string",
                "description": "The ledger balance (accrued − used). It still counts hours waiting for a decision — show available_to_request_hours as what a new request can use.",
                "example": "10.00"
              },
              "pending_hours": {
                "type": "string",
                "description": "Hours in this type's requests that are waiting for a decision.",
                "example": "4.00"
              },
              "available_to_request_hours": {
                "type": "string",
                "description": "Hours a new request can draw today — the ledger balance less pending requests, the figure the server books a request against (the request form's \"hours left to request\").",
                "example": "6.00"
              },
              "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 Forms to Fill Out 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"
          },
          "display_state": {
            "type": "string",
            "description": "The state the web pages show: the stored status, except `completed` for a sent submission whose form needs no approval.",
            "enum": [
              "draft",
              "submitted",
              "under_review",
              "approved",
              "rejected",
              "pending_completion",
              "changes_requested",
              "completed"
            ],
            "example": "completed"
          },
          "status_label": {
            "type": "string",
            "description": "Human-readable label for the status badge (of `display_state`)",
            "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"
        ]
      },
      "NewsFeedNotificationPreference": {
        "type": "object",
        "description": "One Communications notification preference of the caller, in this business.",
        "properties": {
          "segment_id": {
            "type": "integer",
            "nullable": true,
            "description": "null for the default preference"
          },
          "operational_frequency": {
            "type": "string",
            "nullable": true,
            "enum": [
              "realtime",
              "hourly",
              "daily",
              null
            ],
            "description": "The caller's own choice for announcements; null = none, the company default applies"
          },
          "effective_operational_frequency": {
            "type": "string",
            "enum": [
              "realtime",
              "hourly",
              "daily"
            ],
            "description": "What the caller actually gets for announcements"
          },
          "social_frequency": {
            "type": "string",
            "enum": [
              "daily",
              "weekly"
            ]
          },
          "persisted": {
            "type": "boolean",
            "description": "false when nothing is saved yet and these are the defaults the caller gets"
          }
        }
      },
      "CommsRequestedAction": {
        "type": "object",
        "description": "The one thing a message (a feed post, a broadcast, a newsletter issue)\nasks THIS reader to do — a form to complete, a survey to take, a course\nor a policy to open — as the web draws it on the message (\"Requested\naction: <title>\" and a button). Carried as `requested_action`, `null`\nunless the message was sent to the caller with one: its author, a\nreader outside its audience and anyone it was not sent to get `null`.\nDraw one of three states: `completed` (\"You have completed this\naction.\"), else `unavailable` (the requested version is no longer open\nto this reader — no button), else a button labelled by `kind` (`form`\n\"Complete form\", `survey` \"Take survey\", `course` \"Open course\",\n`policy` \"Open policy\") that opens `url`.\n",
        "properties": {
          "title": {
            "type": "string",
            "nullable": true,
            "description": "The form, survey, course or policy's name."
          },
          "kind": {
            "type": "string",
            "enum": [
              "form",
              "survey",
              "course",
              "policy"
            ]
          },
          "completed": {
            "type": "boolean"
          },
          "unavailable": {
            "type": "boolean"
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the page that completes the action — the phone\npage where it has one. Non-null only when the action is neither\ncompleted nor unavailable.\n"
          }
        },
        "required": [
          "title",
          "kind",
          "completed",
          "unavailable",
          "url"
        ],
        "example": {
          "title": "Ladder safety check",
          "kind": "form",
          "completed": false,
          "unavailable": false,
          "url": "https://acme.workforce.mangoapps.com/m/apps/forms/form/5/fill"
        }
      },
      "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",
              "company_news",
              "social"
            ],
            "description": "What the post is about. `operational` and `social` are derived\nfrom `priority`; `company_news` is chosen by the author on an\nOperational or Must Read post (a result, a new customer, an office\nopening, an award) and gives the post its own Company News tab and\nfilter. Never combined with `priority: social`.\n"
          },
          "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 `priority ==\n\"operational\"` on a composed post, 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\nA broadcast-contributed post is stored at priority `operational`\nbut is a Broadcast to its readers, so it reports `false` here\n(since 2026-10-09; it reported `true` before) and `kind:\n\"break_through\"`. Prefer `kind` / `kind_label` for the badge.\n",
            "example": true
          },
          "kind": {
            "type": "string",
            "enum": [
              "post",
              "announcement",
              "must_read",
              "break_through"
            ],
            "description": "The post's kind as Compose names it (Comms::MessageKinds.kind_for):\n`post`, `announcement`, `must_read`, or `break_through` for the\npost a Broadcast contributed to News. Draw the badge from this\nand `kind_label` instead of a priority→kind table.\n",
            "example": "announcement"
          },
          "kind_label": {
            "type": "string",
            "nullable": true,
            "description": "The reader's word for `kind` — the company's own label for posts\nand announcements when it chose one (Communications settings),\nelse the kind's name (\"Post\", \"Announcement\", \"Must-read\",\n\"Broadcast\").\n",
            "example": "Announcement"
          },
          "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"
            ]
          },
          "featured": {
            "type": "object",
            "nullable": true,
            "description": "The post's current Top of News booking for the caller, or null. Since\nKnowledge Layer Phase 3 (E7) featuring is a booking on the one\nplacement engine; the shape is unchanged. Editors manage it through\n`/feeds/{id}/feature`.\n",
            "properties": {
              "placement_id": {
                "type": "integer",
                "description": "The booking — what `POST /feeds/{id}/placements/{placement_id}/dismiss` and `…/end_now` name."
              },
              "starts_at": {
                "type": "string",
                "format": "date-time"
              },
              "ends_at": {
                "type": "string",
                "format": "date-time"
              },
              "position": {
                "type": "integer"
              },
              "can_end_now": {
                "type": "boolean",
                "description": "Detail only (`GET /feeds/{id}`): offer End now — the caller may\nend this booking (the person who placed it, the author, or one the\nbooking rule lets place it) and the token holds\n`write:news_feed_editorial_features`. Absent on list rows.\n"
              }
            }
          },
          "can_feature": {
            "type": "boolean",
            "nullable": true,
            "description": "Detail only (`GET /feeds/{id}`): offer Feature on top — the post is\nout and not expired, the token holds\n`write:news_feed_editorial_features`, and the caller may feature it\nfor everyone it reaches (the rule `PATCH /feeds/{id}/feature` asks).\nNull on list rows, where it is not worked out per post.\n"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "On a broadcast-sourced post, a recipient whose language differs reads\nthe title in their language (never the author); `original_title`\nthen carries what was written.\n"
          },
          "body": {
            "type": "string",
            "nullable": true,
            "description": "Markdown. A post stored as HTML (a newsletter's in-app copy) is\nwritten out as Markdown, with its pictures at addresses signed for\nan hour; the HTML a web view draws is `rendered_body` on the detail.\nTranslated like `title`. An unchecked machine translation ends with a\nshort \"Machine translation.\" note in the reader's language; a checked\none carries no note.\n"
          },
          "newsletter": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NewsFeedNewsletterCopy"
              }
            ],
            "nullable": true,
            "description": "Set only on a newsletter's in-app copy (`source.type` is\n`CommsHub::Issue`); null on every other post. The same words as\n`body`, cut at each hero banner so the banner's title, subtitle and\nbuttons can be drawn ON its picture, as the web and the email draw\nthem. A client that draws `newsletter.blocks` draws them instead of\n`body`, and makes no link or file card of its own for the links and\npictures in them — they are part of the newsletter. In the\nreader's language it is `reader_translation.newsletter`, as `body`\nis `reader_translation.body`. Added 2026-10-16; a client that does\nnot read it keeps drawing `body`.\n"
          },
          "translated": {
            "type": "boolean",
            "description": "Whether `title` / `body` are a translation (then `original_title` / `original_body` are set)."
          },
          "translation_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "checked",
              "ai_draft",
              null
            ],
            "description": "The one reader label's facts (Knowledge Layer P6): `checked` — a\nperson checked this translation (`translation_checked_by` names\nthem: \"Translated from English · Checked by …\"); `ai_draft` — AI\ntranslated it and nobody has checked it yet (\"Translated by AI — not\nyet checked\"). Null when the caller reads the original.\n"
          },
          "translation_checked_by": {
            "type": "string",
            "nullable": true,
            "description": "Who checked the translation — only when `translation_status` is `checked`.",
            "example": "Marie Dubois"
          },
          "original_title": {
            "type": "string",
            "nullable": true,
            "description": "The title as written — only when `translated`."
          },
          "original_body": {
            "type": "string",
            "nullable": true,
            "description": "The body in the language it was written in, Markdown like `body` — only when `translated`."
          },
          "reader_translation": {
            "type": "object",
            "nullable": true,
            "description": "The post in the caller's own language, when the company translates\nposts for readers (\"Translate posts for readers\" in Communications\nsettings). Null when that is off, when the caller reads English or a\nlanguage the company does not offer, on the caller's own post, on a\ndraft or scheduled post, and on a broadcast's post whose `title` /\n`body` already carry the translation (`translated`). `title` and\n`body` above stay as written: show this block's words in their place\nand offer \"Show original\". A `pending` translation is being written\nnow — ask again shortly; reading the post is what starts it.\n",
            "properties": {
              "language": {
                "type": "string",
                "example": "ko"
              },
              "state": {
                "type": "string",
                "enum": [
                  "translated",
                  "pending",
                  "awaiting_review",
                  "failed"
                ],
                "description": "`translated` — `title` / `body` hold the translation.\n`pending` — being translated now; show the original meanwhile.\n`awaiting_review` — a Must-Read whose translation a person must\ncheck first; show the original and say so.\n`failed` — it could not be translated right now; show the original.\n"
              },
              "title": {
                "type": "string",
                "nullable": true,
                "description": "The translated title — only when `translated`."
              },
              "body": {
                "type": "string",
                "nullable": true,
                "description": "The translated body (Markdown, like `body`) — only when `translated`."
              },
              "newsletter": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NewsFeedNewsletterCopy"
                  }
                ],
                "nullable": true,
                "description": "A newsletter's in-app copy, translated and cut at its hero\nbanners like the post's own `newsletter` — only when `translated`\nand the post is a newsletter copy. A client that draws\n`newsletter.blocks` draws these in their place, as it draws this\n`body` in place of the post's.\n"
              },
              "poll_options": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "A poll's choices in the caller's language, keyed by option id —\nonly when `translated`. Empty for any other post.\n"
              },
              "label": {
                "type": "object",
                "nullable": true,
                "description": "The one reader label's facts — only when `translated`.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "checked",
                      "ai_draft",
                      "out_of_date"
                    ]
                  },
                  "checked_by": {
                    "type": "string",
                    "nullable": true
                  },
                  "source_language": {
                    "type": "string",
                    "example": "en"
                  }
                }
              }
            }
          },
          "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,
            "description": "When the post leaves the feed — for a must-read, when it stops\nasking for acknowledgement, which can be days after `due_at`.\n"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "A must-read's acknowledge-by moment: the author's \"Due by\", else\n`expires_at`. Readers who have not acknowledged by then are\noverdue while the post still asks (until `expires_at`). null for a\npost that asks no acknowledgement. The caller's OWN date — moved\non for someone the post reached late — is\n`acknowledgement_prompt.due_at`.\n"
          },
          "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-attachment controls instead of guessing; the server\nre-enforces it on every write regardless. False for an API token\nwithout write:news_feed.\n"
          },
          "can_remove_attachment": {
            "type": "boolean",
            "description": "Whether the caller may remove one of this post's attachments\n(`DELETE /feeds/{feed_id}/media/{media_id}`): the same rule as\n`editable`, with a credential holding destructive:news_feed, which\nthat delete-level door asks (a signed-in session always does).\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": "The post's audience, named, so clients can render it without a\nsecond roundtrip; `[]` means everyone. For a post sent to segments it\nis parallel to `audience_segment_ids`: numeric ids resolve via\n`NotificationRecipientGroup` (`name` is `null` once the group is\ndeleted) and the `direct_reports` pseudo-segment resolves to\n\"Author's direct reports\". A post sent to a saved audience is one\nentry, id `audience:<id>`. A post sent to specific people or groups\nhas one entry per group (its bare id), department (`department:<id>`),\nplace (`location:<id>`, or `location_only:<id>` named \"<place> only\"),\njob title (`job_title:<title>`), role (`role:<role>`), team\n(`reports_of:<user id>`) and attribute filter (`attributes:<n>`) it\npicked, and the people it picked by name as one entry, id `people`,\nnamed \"N people\".\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "audience_size": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "The post's audience: the people its audit-grade audience\nsnapshot names — the same figure as `metrics.audience_size` on\n`GET /api/v1/news-feed/feeds/{id}/analytics`, and the set\n`acknowledged_count` is counted within — falling back to the\nreach captured at publish time for a post with no snapshot.\nWhile the post's works-council group is withheld it counts the\nnamed people only, as the read and acknowledged counts do. Pair\nwith `acknowledged_count` for an \"N of M acknowledged\" badge. It\ngrows when a person who joined the audience after publish\nacknowledges the post or reads it to the end.\n`null` for an unsent post, or one that predates both.\n"
          },
          "author_id": {
            "type": "integer",
            "description": "Convenience alias for `author.id` (kept for backwards-compat)."
          },
          "author": {
            "$ref": "#/components/schemas/NewsFeedUserRef"
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Draw the \"AI-assisted\" label. Resolved for the CALLER (Knowledge\nLayer E6): true only when the post was written with AI help AND\nthe caller may be told — the company shows it to readers\n(Communications › Show readers when AI helped write a message), or\nthe caller is the author, the delegate who sent it or a moderator.\n"
          },
          "sent_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NewsFeedUserRef"
              }
            ],
            "nullable": true,
            "description": "The delegate who sent this post as its author (\"sent by …\"), under\nthe same caller-resolved rule (Communications › Show readers who\nsent a message on someone's behalf). `null` when there is none or\nthe caller may not be told.\n"
          },
          "via": {
            "type": "string",
            "nullable": true,
            "example": "Contests",
            "description": "The app that created this post under the author's name (\"via\nContests\"). Always shown; `null` for a person's own post and for a\nbroadcast's or newsletter's own feed post.\n"
          },
          "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
            }
          },
          "requested_action": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CommsRequestedAction"
              }
            ],
            "nullable": true,
            "description": "The one thing this post asks THIS reader to do, as the web draws it\non the post. `null` unless the post was sent to the caller with one.\n"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "can_engage": {
            "type": "boolean",
            "description": "Whether the caller may engage with this post — react, comment,\nmute, unpin, mark seen, complete an action, vote, acknowledge. Hide\nthose controls when false. False on a draft or scheduled post (its\npreparers may read it; nobody engages until it goes out), and for\na reader outside the post's audience — the holder of the \"view\nany post\" grant, or a Communications admin moderating it — whom\nthose endpoints answer 403. For such a reader\n`acknowledgement_prompt` is also null and `poll.allow_comments`\nfalse. False too for an API token without write:news_feed, which\nevery one of those doors asks; for it `poll.allow_comments` is false\nand `acknowledgement_prompt.can_acknowledge` false (the prompt still\nsays what the person owes).\n"
          },
          "can_pin": {
            "type": "boolean",
            "description": "Whether the caller may pin this post (`POST /feeds/{id}/pin`) —\n`can_engage`, and the post is one the `filter=pinned` list shows:\npublished and not expired. An expired or archived post answers 422\n`not_pinnable`; Unpin stays open on `can_engage` so a stale pin can\nbe removed.\n"
          },
          "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"
          },
          "reactions_enabled": {
            "type": "boolean",
            "description": "False on a post that takes no reactions — a broadcast's News post\nwhose broadcast turned reactions off, or that is archived. POST\n/feeds/{id}/reactions then refuses a new reaction (422\n`reactions_closed`); taking your own off still works.\n"
          },
          "comments_allowed": {
            "type": "boolean",
            "description": "Whether `POST /feeds/{id}/comments` takes a comment on this post:\nthe post has comments on and nobody closed its discussion\n(`discussion_open`). False on a post written with comments off,\nand on a recognition's mirrored post, whose comments live on the\nrecognition (`social_thread`) — POST …/comments answers 403\n`discussion_closed` there. Show a comment box only when this,\n`can_engage` and the tenant's `comments_enabled` and\n`discussion_layer_enabled` (`GET /news-feed/settings`) are all\ntrue. Same rule as `poll.allow_comments`.\n"
          },
          "social_thread": {
            "type": "object",
            "nullable": true,
            "description": "Set on a post that mirrors another record's reactions and comments\n— today a public recognition (`RecognitionPost`) or an award\n(`Award`) on the News Feed. The post has no thread of its own:\n`reactions` above is that record's set, and its comments are read\nand written on the record's page (`path`). `null` on every post\nthat owns its own reactions and comments.\n",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "RecognitionPost",
                  "Award"
                ],
                "description": "The record that owns the thread."
              },
              "id": {
                "type": "integer",
                "description": "That record's id."
              },
              "path": {
                "type": "string",
                "description": "The record's own page, where its comment thread lives (`/recognition/posts/77`)."
              },
              "comments_count": {
                "type": "integer",
                "minimum": 0,
                "description": "Comments on that record's thread."
              }
            },
            "required": [
              "type",
              "id",
              "path",
              "comments_count"
            ],
            "example": {
              "type": "RecognitionPost",
              "id": 77,
              "path": "/recognition/posts/77",
              "comments_count": 4
            }
          },
          "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": "Distinct people who have opened this post (any view — the author\nand people outside its audience included). Not the numerator of a\nread rate: a post's \"Read by N of M\" is `metrics.read_count` of\n`metrics.audience_size` on\n`GET /api/v1/news-feed/feeds/{id}/analytics`. Someone who joins the\naudience after publish and reads it to the end is added to both.\n"
          },
          "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"
          },
          "question_state": {
            "$ref": "#/components/schemas/NewsFeedQuestionState"
          },
          "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"
              }
            }
          },
          "acknowledgement_prompt": {
            "type": "object",
            "nullable": true,
            "description": "What the caller still owes on a must-read — the Acknowledge prompt\nthe web and phone strips draw. null when they owe nothing (not a\nmust-read that asks, already acknowledged, accepted the policy it\nnames, the author, outside its audience, or it stopped asking).\n",
            "properties": {
              "required": {
                "type": "boolean",
                "description": "Always true when the prompt is present."
              },
              "can_acknowledge": {
                "type": "boolean",
                "description": "Whether THIS request may make the acknowledgement\n(`POST /feeds/{id}/acknowledge`): false for a token without\nwrite:news_feed and while viewing as someone else. Draw the\nAcknowledge button only when true; the obligation stands either way.\n"
              },
              "title": {
                "type": "string"
              },
              "guidance": {
                "type": "string"
              },
              "voice_unavailable_guidance": {
                "type": "string"
              },
              "acknowledge_label": {
                "type": "string",
                "example": "Acknowledge"
              },
              "due_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the caller must acknowledge by — the post's `due_at`,\nmoved on for someone it reached late, so the app names the\ndate the web strip and Needs you name.\n"
              },
              "overdue": {
                "type": "boolean",
                "description": "True once `due_at` has passed while the post still asks."
              },
              "due_text": {
                "type": "string",
                "nullable": true,
                "description": "The strip's own words for that date, in the caller's language and\ntime zone — \"Due Fri, Oct 30 at 5:00 PM\", or \"Was due …\" once it\nhas passed. null when there is no date.\n",
                "example": "Due Fri, Oct 30 at 5:00 PM"
              }
            }
          },
          "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. The post's cover\nimage is in this list too, marked `cover: true`, and a picture\ndrawn inside `body` is marked `inline: true` (leave it out of the\ngallery). A newsletter's in-app copy lists none of the pictures in\nits body: they are the newsletter's, drawn in its words.\n",
            "items": {
              "$ref": "#/components/schemas/NewsFeedFeedMedia"
            }
          },
          "cover_image": {
            "type": "object",
            "nullable": true,
            "description": "The post's cover image (the composer's \"Featured image\"): draw it\nat the top of the post — after the author, before the title — at\nits own shape (`width` / `height`, null when unknown), never\ncropped, and leave it out of the `media` gallery. Null when the\npost has none. `url` is short-lived like every media URL; refetch\nthe feed on a 403. A tap on the cover opens `link` when it has\none, else the picture full size.\n",
            "properties": {
              "media_id": {
                "type": "integer",
                "description": "The `media` item it is."
              },
              "url": {
                "type": "string"
              },
              "alt": {
                "type": "string",
                "nullable": true,
                "description": "Alt text; null when none was written."
              },
              "width": {
                "type": "integer",
                "nullable": true
              },
              "height": {
                "type": "integer",
                "nullable": true
              },
              "link": {
                "type": "string",
                "nullable": true,
                "description": "Where a tap on the cover goes on a \"Tell people\" post — the\ncourse, wiki or Sites page the post announces (its cover is\nthat record's own picture), the same in-app path as the post's\nown \"Open the course\" link in `body`, relative to the tenant's\nhost. Null on every other post.\n",
                "example": "/apps/training/learner/courses/42?from=catalog"
              }
            }
          },
          "inline_media_ids": {
            "type": "array",
            "description": "Detail only. Ids of `media` pictures the rendered body already shows\ninline — leave them out of the gallery.\n",
            "items": {
              "type": "integer"
            }
          }
        },
        "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
          }
        }
      },
      "FeedWhereChoice": {
        "type": "object",
        "description": "The phone composer's \"Where it shows\" ticks (`GET /feeds/where_it_shows`\nlists what is offered). Top of Home starts at the first free moment for\nthese people and runs 7 days; a store pin runs to the end of its day.\n",
        "properties": {
          "top_of_home": {
            "type": "boolean"
          },
          "store_pin": {
            "type": "boolean"
          },
          "store_pin_until": {
            "type": "string",
            "format": "date",
            "description": "The day the pin runs to; default tomorrow."
          }
        }
      },
      "FeedWhereItShowsResult": {
        "type": "object",
        "description": "Present when the request sent `where` and a place was in play.",
        "properties": {
          "placed": {
            "type": "boolean",
            "description": "False when a place was taken by someone else a moment before the write; the post went out without it."
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "What was placed or taken off, or why it isn't placed."
          }
        }
      },
      "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"
              },
              "campaign": {
                "nullable": true,
                "allOf": [
                  {
                    "type": "object",
                    "description": "A Communications campaign as a composer draws it (the Campaign field).",
                    "required": [
                      "id",
                      "name",
                      "status",
                      "kickoff_at",
                      "ended_at",
                      "open",
                      "can_change"
                    ],
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "draft",
                          "active",
                          "completed",
                          "canceled"
                        ],
                        "description": "Follows the campaign's dates (a Draft turns Active on its kickoff date, an Active one Completed the day after it ends), unless a person set it."
                      },
                      "kickoff_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "ended_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "open": {
                        "type": "boolean",
                        "description": "Draft or Active — it can take new messages."
                      },
                      "can_change": {
                        "type": "boolean",
                        "description": "Whether the caller may take a message out of it (false for a token without `write:broadcasts`). False: show its name instead of a choice, as the web does."
                      }
                    }
                  }
                ],
                "description": "The Communications campaign the post is in (a newsletter's\nin-app copy: its issue's) — for its author or a caller who may\nchange that campaign; otherwise `null`, as for a post in no\ncampaign. `can_change` is false for a token without\n`write:broadcasts`. Detail-only.\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"
          },
          "marked_by": {
            "nullable": true,
            "type": "object",
            "description": "Who marked the answer — the card's \"marked by <name>\". `name` is\nthe person's name, else \"Unnamed user\"; `null` when that account\nis gone.\n",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "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"
              }
            }
          }
        }
      },
      "NewsFeedQuestionState": {
        "type": "object",
        "description": "Question posts only. Where the question stands — the marks the web and\nphone post foot draws, from the same code (NewsFeed::QuestionState).\nRead live: marking or unmarking an answer changes it on the next read.\n",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "open",
              "routed",
              "answered",
              "answered_by_expert",
              "outdated"
            ],
            "description": "The first mark: `outdated` (an admin marked the verified answer out\nof date), `answered_by_expert` (the marked answer is the reply of an\nexpert the question was routed to), `answered` (any other marked\nanswer), `routed` (sent to experts, none of whose replies is the\nanswer), else `open`.\n"
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "The first mark's words as the web shows them, in the caller's language (\"Answered by Dana Lee\", \"Sent to an expert\"); `null` when `open`."
          },
          "expert": {
            "nullable": true,
            "type": "object",
            "description": "The expert whose reply is the marked answer; `null` otherwise (a routed expert stays unnamed until they answer).",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "experts_asked": {
            "type": "integer",
            "minimum": 0,
            "description": "How many experts the question was first sent to."
          },
          "marks": {
            "type": "array",
            "description": "Every mark the post foot draws, in its order — Outdated can sit beside Answered by an expert, and Answered beside Sent to experts.",
            "items": {
              "type": "object",
              "properties": {
                "state": {
                  "type": "string",
                  "enum": [
                    "routed",
                    "answered",
                    "answered_by_expert",
                    "outdated"
                  ]
                },
                "label": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "NewsFeedTopicFollowResponse": {
        "type": "object",
        "properties": {
          "topic": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "disabled"
                ]
              },
              "managed": {
                "type": "boolean"
              },
              "following": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "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
          },
          "can_vote": {
            "type": "boolean",
            "description": "Whether POST (a first vote) or PATCH (a change) on\n`/feeds/{id}/poll_votes` would take this caller's ballot now — the\ndoor's own rule: never through an impersonation token or under\nView-as, on a draft, a closed poll, a vote that may not change, or\nfor a reader outside the audience. Draw the ballot only when true.\n"
          },
          "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"
          },
          "voters_visible": {
            "type": "boolean",
            "description": "True when `GET /feeds/{id}/poll_option_voters` answers this caller\n— draw \"See who voted\" exactly then. Always false on an anonymous\npoll (`is_anonymous`); on a named poll it follows the\n`results_visible` rules above. The same rule as the web and phone\n\"See who voted\" and the privacy sentence under every poll.\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"
          },
          "held_for_review": {
            "type": "boolean",
            "description": "Waiting for a moderator. Only its author is ever sent such a comment."
          },
          "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,
                "format": "uri",
                "description": "Full URL of the author's avatar — their uploaded photo on the API's own host, else an initials tile."
              }
            }
          },
          "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"
          },
          "reader_translation": {
            "type": "object",
            "nullable": true,
            "description": "The comment in the caller's language, when the company offers\nreader translation and the comment is in another language (filled\non `GET /feeds/{id}/comments` for the whole page; `null` on\ncreate/update, on the caller's own comment, a deleted or\npicture-only comment, and when no translation applies — including\na comment already written in the caller's language).\n`state`: `translated` (show `body` with \"Translated by AI · Show\noriginal\"), `pending` (being translated — ask again shortly),\n`awaiting_review`, or `failed` (show the original).\n",
            "properties": {
              "language": {
                "type": "string",
                "example": "ko"
              },
              "state": {
                "type": "string",
                "enum": [
                  "translated",
                  "pending",
                  "awaiting_review",
                  "failed"
                ]
              },
              "body": {
                "type": "string",
                "nullable": true,
                "description": "The translated text (mention tokens kept), only when `state` is `translated`."
              },
              "label": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "status": {
                    "type": "string",
                    "example": "ai_draft"
                  },
                  "checked_by": {
                    "type": "string",
                    "nullable": true
                  },
                  "source_language": {
                    "type": "string",
                    "example": "en"
                  }
                }
              }
            }
          },
          "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"
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Draw the \"AI-assisted\" label on this comment. Resolved for the\ncaller (Knowledge Layer E6): true only when it was written with AI\nhelp and the company shows that to readers, or the caller wrote it\nor moderates. Always false on a deleted comment.\n"
          },
          "expert_answer": {
            "nullable": true,
            "description": "\"Did this solve it?\" on a Question post. Set on a top-level\ncomment written by an expert the question was sent to: `open`\nfor the asker while they are being asked about it, `not_solved`\nfor every reader once the asker said it didn't. `null` on every\nother comment — always on a reply and on a deleted comment, on an\nexpert's comment the asker is not being asked about (a correct\nanswer is marked, the discussion is closed, the question has\nexpired or been archived, or they already answered about that\nexpert), and for every reader but the asker while the question is\nstill open.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/NewsFeedExpertAnswer"
              }
            ]
          }
        }
      },
      "NewsFeedExpertAnswer": {
        "type": "object",
        "description": "Where the asker's \"Did this solve it?\" stands on one reply from an\nexpert the question was sent to, and what the caller may answer.\n",
        "required": [
          "state",
          "can_mark_correct",
          "can_mark_not_solved"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "open",
              "not_solved"
            ],
            "description": "`open` — the asker is still being asked whether this reply solved\nit; returned to the asker only, with both `can_*` true.\n`not_solved` — the asker said it didn't: every reader of the\npost is shown \"The asker says this didn't solve it\" (the asker,\n\"You said this didn't solve it\"). It stays on that reply until an\nanswer from the same expert is marked correct.\n"
          },
          "can_mark_correct": {
            "type": "boolean",
            "description": "Offer the caller \"Mark as correct\" — `POST /feeds/{id}/correct_answer`\nwith this comment's id. True only for the asker, while `state` is\n`open` and the post takes engagement (`can_engage`).\n"
          },
          "can_mark_not_solved": {
            "type": "boolean",
            "description": "Offer the caller \"This didn't solve it\" —\n`POST /feeds/{feed_id}/comments/{id}/not_solved`. True only for\nthe asker, while `state` is `open` and the post takes engagement\n(`can_engage`).\n"
          }
        }
      },
      "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."
          },
          "playback_url": {
            "type": "string",
            "nullable": true,
            "description": "Signed URL of the 720p H.264 MP4 playback rendition, when the video has one. Players should prefer it and fall back to the original (`storage_url` / `url`). Null while processing, and for uploads that already play everywhere."
          },
          "hls_playlist_url": {
            "type": "string",
            "nullable": true,
            "deprecated": true,
            "description": "No longer produced; present only on videos transcoded before 2026-09."
          },
          "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"
          },
          "cover": {
            "type": "boolean",
            "description": "The post's cover image — draw it at the top of the post (see the feed's `cover_image`), not in the gallery. Always false on a comment attachment."
          },
          "inline": {
            "type": "boolean",
            "description": "A post's picture drawn inside its text (`body`) — leave it out of the gallery under the post, as the web does. Sent on a post's `media` only."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NewsFeedNewsletterCopy": {
        "type": "object",
        "description": "A newsletter's in-app copy as blocks, in order (NewsFeed::NewsletterCopyForClients).\nJoined, the `markdown` blocks and the heroes say what the post's `body` says.\n",
        "properties": {
          "issue_id": {
            "type": "integer",
            "description": "The newsletter issue this post is the copy of."
          },
          "blocks": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "markdown",
                    "hero"
                  ],
                  "description": "`markdown`: words and pictures between two heroes, written as `body` writes them. `hero`: a banner."
                },
                "markdown": {
                  "type": "string",
                  "description": "Markdown blocks only. Pictures at addresses signed for an hour."
                },
                "image_url": {
                  "type": "string",
                  "nullable": true,
                  "description": "Hero only. The picture the words sit on, cover-fit to the banner (signed for an hour); null when the banner has none — draw `ground`."
                },
                "image_alt": {
                  "type": "string",
                  "nullable": true
                },
                "title": {
                  "type": "string",
                  "nullable": true,
                  "description": "Hero only. The banner's headline."
                },
                "subtitle": {
                  "type": "string",
                  "nullable": true,
                  "description": "Hero only. The line under the headline."
                },
                "align": {
                  "type": "string",
                  "enum": [
                    "left",
                    "center",
                    "right"
                  ],
                  "description": "Hero only. Where the words sit."
                },
                "look": {
                  "type": "string",
                  "enum": [
                    "dark",
                    "light",
                    "brand"
                  ],
                  "description": "Hero only. `dark` / `light`: the measured tone of the picture under the words. `brand`: no picture, the company's brand fill."
                },
                "ink": {
                  "type": "string",
                  "description": "Hero only. The colour of the title and subtitle (hex).",
                  "example": "#ffffff"
                },
                "ground": {
                  "type": "string",
                  "description": "Hero only. The fill behind the banner — under a picture still loading, or the whole banner when it has none. A hex colour, or `rgba(r, g, b, a)`.",
                  "example": "#9c421a"
                },
                "shade": {
                  "type": "string",
                  "nullable": true,
                  "description": "Hero only. A colour laid over the picture under the words — full strength behind them, fading out across the banner (toward the far side when the words sit left or right) — so they read; null when the picture needs none.",
                  "example": "rgba(0, 0, 0, 0.6)"
                },
                "buttons": {
                  "type": "array",
                  "description": "Hero only. The banner's buttons, under the subtitle.",
                  "items": {
                    "type": "object",
                    "properties": {
                      "label": {
                        "type": "string"
                      },
                      "url": {
                        "type": "string"
                      },
                      "fill": {
                        "type": "string",
                        "description": "The button's colour (hex)."
                      },
                      "ink": {
                        "type": "string",
                        "description": "The label's colour (hex)."
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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"
          },
          "confidential": {
            "type": "boolean",
            "description": "Marked Confidential — the badge the desktop and phone incident pages draw. A label only: it does not change who can open the incident. A confidential incident is not offered as evidence in Compliance Hub."
          },
          "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\n**`can_edit` and `can_manage_investigation` answer for the request's\ntoken as well as the person**, as `SafetyHubPermitPermissions` does:\neach is true only when the endpoints behind it would accept this caller\nand this token — the caller's role and site, the token ceiling (acting\non someone else's incident through manager privilege needs the wide\n`write:safety_hub`; a `write:own_safety_hub` token reaches only the\ncaller's own report) and each endpoint's scope. `can_edit` also asks the\nincident's state; `can_manage_investigation` does not (see its\ndescription). Gate each control on its flag, never on a role. A session (cookie) caller has no token, so only\nthe role, site and state rules apply. `can_view_investigation`\ndescribes data this response already carries, and `can_close` /\n`close_blocker` describe the web's Close Incident control (there is no\nAPI close endpoint), so those do not depend on the token.\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's API actions — list the candidate investigators (GET .../investigators), reassign the investigator (PATCH .../investigator) and complete the investigation (POST .../complete_investigation): a manager at the incident's site whose token holds the wide `read:safety_hub` and `write:safety_hub`. False whenever any of those endpoints would refuse this caller or this token. It does not read the incident's status, which still decides which of them applies: reassigning needs a `reported` or `investigating` incident (the investigators list's `reassignable`) and completing needs `investigating` — otherwise each answers 422."
          },
          "can_edit": {
            "type": "boolean",
            "description": "Viewer may edit the incident (PATCH /safety_hub/incidents/{id}) — its reporter, or a manager at its site — the incident is in an editable state, and the token holds the write scope (the wide `write:safety_hub` when a manager edits someone else's report)."
          },
          "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. Describes the web control — there is no API close endpoint — so it does not depend on the token."
          },
          "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"
          },
          "can_cancel": {
            "type": "boolean",
            "description": "Viewer may cancel the report now through POST /safety_hub/incidents/{id}/cancel: its reporter while it is still reported, or a site manager, nothing blocks the cancel, and the token holds destructive:safety_hub."
          },
          "cancel_hint": {
            "type": "string",
            "nullable": true,
            "description": "Why a viewer who may cancel this report cannot yet — it has open\ncorrective actions, each to be completed or cancelled first — or null.\nThe web pages show it in place of the Cancel control. Null whenever\ncan_cancel is true, the viewer may never cancel it, or the token lacks\ndestructive:safety_hub.\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. Chosen once: it cannot be changed after the incident is reported."
              },
              "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
                },
                "user_id": {
                  "type": "integer",
                  "nullable": true,
                  "description": "The person's account in this business, when they work here. An injured person linked to their account is told and can open their own injury record (GET /safety_hub/incidents/{id} returns own_injury_record to them)."
                }
              }
            }
          },
          "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"
              },
              "reported_severity": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "Low",
                  "Medium",
                  "High",
                  "Critical"
                ],
                "description": "Severity a public-portal visitor chose; null for any other observation."
              },
              "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"
              }
            }
          }
        ]
      },
      "SafetyObservationPermissions": {
        "type": "object",
        "description": "Viewer-relative capability flags returned beside an observation detail\n(GET / PATCH /safety_hub/observations/{id} and POST\n.../complete_follow_up), so a client renders the \"Edit observation\" and\n\"Complete Follow-up\" controls without a second round trip.\n\n**Each `can_*` flag answers for the request's token as well as the\nperson**, exactly as `SafetyHubPermitPermissions` does: it is true only\nwhen this request could take that action now — the caller's role and\nsite, the observation's state, the token ceiling (acting on someone\nelse's observation through manager privilege needs the wide\n`write:safety_hub`; a `write:own_safety_hub` token reaches only the\ncaller's own observations) and the endpoint's `write:safety_hub` scope.\nGate a control on its flag, never on `is_manager`. A session (cookie)\ncaller has no token, so only the role, site and state rules apply.\n",
        "properties": {
          "is_observer": {
            "type": "boolean",
            "description": "The caller submitted this observation. Describes the viewer, not an action, so it does not depend on the token."
          },
          "is_manager": {
            "type": "boolean",
            "description": "The caller is a Safety Hub manager at the observation's site. Describes the viewer, not an action, so it does not depend on the token."
          },
          "can_edit": {
            "type": "boolean",
            "description": "The caller may edit the observation (PATCH /safety_hub/observations/{id}) — its observer, or a manager at its site — with a token that holds the write scope, and the wide `write:safety_hub` when a manager edits someone else's observation."
          },
          "can_complete_follow_up": {
            "type": "boolean",
            "description": "The caller may complete the follow-up (POST .../complete_follow_up): one is outstanding, and the caller is a manager at the observation's site whose token holds the wide `write:safety_hub`. False once the follow-up is complete."
          }
        }
      },
      "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,
                "enum": [
                  "Not started",
                  "In progress",
                  "Completed",
                  "Cancelled"
                ],
                "description": "Capa::Action#status_label — `pending` is \"Not started\". An action\nreads \"In progress\" only once someone starts the work\n(POST .../corrective_actions/{id}/start); reassigning it never\nmoves the status.\n"
              },
              "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\n**`can_complete` and `can_assign` answer for the request's token as\nwell as the person**, as `SafetyHubPermitPermissions` does: each is true\nonly when this request could take that action now — the caller's role,\nthe action's state, the token ceiling (acting on someone else's action\nthrough manager privilege needs the wide `write:safety_hub`; a\n`write:own_safety_hub` token reaches only the caller's own actions) and\nthe endpoint's `write:safety_hub` scope. Gate each control on its flag,\nnever on `is_manager`. A session (cookie) caller has no token, so only\nthe role and state rules apply. `can_verify` describes the web\nregister's Verify control (there is no API verify endpoint), so it does\nnot depend on the token.\n",
        "properties": {
          "is_assignee": {
            "type": "boolean",
            "description": "True when the caller is the action's assignee. Describes the viewer, not an action, so it does not depend on the token."
          },
          "is_manager": {
            "type": "boolean",
            "description": "True when the caller is a safety-hub manager (manager-or-above OR the app admin). Describes the viewer, not an action, so it does not depend on the token."
          },
          "can_start": {
            "type": "boolean",
            "description": "True when the action is Not started and the caller may start work on it (POST .../corrective_actions/{id}/start) — the same people, token and scope rule as `can_complete`."
          },
          "can_complete": {
            "type": "boolean",
            "description": "True when the action is OPEN and the caller may mark it complete (POST .../corrective_actions/{id}/complete): its assignee, or a manager whose token holds the wide `write:safety_hub` — with a token that holds the write scope either way. False while `completion_photo_required`."
          },
          "completion_photo_required": {
            "type": "boolean",
            "description": "True when the action comes from an inspection, the company requires a photo of the completed work to complete one (Settings › Inspections), and none is attached yet. This endpoint takes no photo, so the action is completed where the photo is taken (the web register, or POST /inspections/corrective_actions/{id}/complete); a completion attempt answers 422 with `error_code: completion_photo_required`."
          },
          "can_assign": {
            "type": "boolean",
            "description": "True when the action is OPEN and the caller may reassign it (PATCH .../corrective_actions/{id}/assignee): a manager whose token holds the wide `write:safety_hub` (`write:own_safety_hub` suffices only for a manager reassigning an action assigned to themselves)."
          },
          "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). Describes the\nweb register's Verify control — there is no API verify endpoint — so\nit does not depend on the token.\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
              },
              "crew": {
                "type": "array",
                "description": "The people named as working under the permit (members of this\nbusiness), by name. Being on the crew lets a person open the\npermit and hear when it is issued, suspended, cancelled, past\nits end time or expired; it grants no edit or close right. Set with\n`work_permit[crew_user_ids][]` on create and update.\n",
                "items": {
                  "$ref": "#/components/schemas/SafetyHubPersonRef"
                }
              },
              "hazards": {
                "type": "array",
                "description": "The permit type's full hazard checklist, each with the hazard\ntext and whether it is checked for this permit. Any hazard\ncarried on the permit but absent from the type catalog is\nappended and always returned checked.\n",
                "items": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "checked": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "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
                  },
                  "lapsed_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "description": "When `current` is false: the date the contractor's latest pre-qualification lapsed (null when it never had one, or it was rejected or never decided)."
                  }
                }
              },
              "certificate_gaps": {
                "type": "array",
                "description": "The contractor's certificates ticked \"Required for site work\" in\nContracts that do NOT cover the work still to come on this permit:\nexpired, expiring before the work ends, taking effect only after\nit starts, or denied in review — each certificate judged on its\nown (a renewal with the same type and title replaces an older\none) — plus \"No certificate of insurance on file\" when no\nreviewed certificate of insurance covers the visit. Empty for an\nown-crew permit and once the visit is over (closed / cancelled /\nexpired, or past its end time). Issuing or resuming a permit with\ngaps needs `proceed_anyway` + `certificate_override_reason` +\n`confirmed_gap_keys`; an issued permit can instead record\nPOST /safety_hub/permits/{id}/confirm_certificate_cover.\n",
                "items": {
                  "$ref": "#/components/schemas/SafetyHubPermitCertificateGap"
                }
              },
              "certificate_override": {
                "nullable": true,
                "type": "object",
                "description": "Present when someone proceeded although the contractor had\ncertificate gaps — the LATEST such confirmation (at issue,\nresume or cover confirmation): the step, the reason, the gaps\nconfirmed, and the person who made that decision.\n",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "issued",
                      "resumed",
                      "cover_confirmed"
                    ]
                  },
                  "reason": {
                    "type": "string"
                  },
                  "gaps": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "certification_id": {
                          "type": "integer",
                          "nullable": true
                        },
                        "cert_type": {
                          "type": "string",
                          "nullable": true
                        },
                        "label": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "expired",
                            "expires_during_work",
                            "not_yet_effective",
                            "rejected",
                            "missing"
                          ]
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "expires_on": {
                          "type": "string",
                          "format": "date",
                          "description": "Present only when the cover is recorded as a date (the pre-qualification insurance)."
                        },
                        "summary": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "confirmed_by": {
                    "$ref": "#/components/schemas/SafetyHubPersonRef"
                  },
                  "confirmed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SafetyHubPermitPermissions": {
        "type": "object",
        "description": "Viewer-relative capability flags returned beside every permit detail\n(GET /safety_hub/permits/{id}, and the response of each permit write), so\na client renders the detail's controls — Save, Request approval, the\nprecaution checklist, Issue, Cancel, Close out, Suspend, Resume, Cover\nconfirmed — without a second round trip.\n\n**Each `can_*` flag answers for the request's token as well as the\nperson** (owner decision 2026-10-06): it is true only when this request\ncould take that action now — the caller's role and site, the permit's\nstate, the token ceiling (acting on someone else's permit through\nmanager privilege needs the wide `write:safety_hub`; a\n`write:own_safety_hub` token reaches only the caller's own permits) and\nthe scope the action's endpoint asks (`write:safety_hub`, or\n`destructive:safety_hub` for cancel, close and suspend). Gate each control on its\nflag, never on `is_manager`: a false flag is a request the server would\nrefuse. A session (cookie) caller has no token, so only the role, site\nand state rules apply.\n",
        "properties": {
          "is_requester": {
            "type": "boolean",
            "description": "The caller raised this permit. Describes the viewer, not an action, so it does not depend on the token."
          },
          "is_manager": {
            "type": "boolean",
            "description": "The caller is a Safety Hub manager at the permit's site. Describes the viewer, not an action, so it does not depend on the token."
          },
          "can_edit": {
            "type": "boolean",
            "description": "The caller may save the permit (PATCH /safety_hub/permits/{id}) — its requester while it is a draft or requested, or a manager at its site while it is open — with a token that holds the write scope, and the wide `write:safety_hub` when a manager saves someone else's permit."
          },
          "can_request_approval": {
            "type": "boolean",
            "description": "The caller may send the permit for approval (POST .../request_approval): the same people and token as `can_edit`, on a `draft` permit."
          },
          "can_confirm_precautions": {
            "type": "boolean",
            "description": "The authoriser-only precaution checklist is editable: `can_edit`, for a manager at the permit's site, while the permit is open."
          },
          "can_approve": {
            "type": "boolean",
            "description": "The caller may issue the permit (POST .../approve): a manager at its site who did not request it, whose token holds the wide `write:safety_hub`, on a `requested` permit whose work window has not ended and — when the tenant requires it — whose contractor has a current pre-qualification. Stays true while precautions are outstanding: send them in the same POST (`work_permit[precautions]`, editable when `can_confirm_precautions`), or the server answers 422. Stays true while the contractor has `certificate_gaps` — issuing then needs `proceed_anyway`, `certificate_override_reason` and `confirmed_gap_keys` (a 422 with `confirmation_required: true` until they are sent): a prompt, not a refusal."
          },
          "can_cancel": {
            "type": "boolean",
            "description": "The caller may cancel the permit (POST .../cancel): the same people as `can_edit` — its requester while it is a draft or requested, or a manager at its site while it is open — on a permit that is not closed, cancelled or expired. Needs `destructive:safety_hub`, and the wide `write:safety_hub` when a manager cancels someone else's permit."
          },
          "can_close": {
            "type": "boolean",
            "description": "The caller may close the permit out (POST .../close) — its requester, its authoriser or a manager at its site — and it is issued, suspended or expired (an expired permit still owes its close-out). Needs `destructive:safety_hub`, and the wide `write:safety_hub` when a manager closes a permit they are not on."
          },
          "can_suspend": {
            "type": "boolean",
            "description": "The caller may suspend the permit (POST .../suspend): a manager at its site, on an issued (`approved`) permit, whose token holds the wide `write:safety_hub` and `destructive:safety_hub`."
          },
          "can_resume": {
            "type": "boolean",
            "description": "The caller may resume the permit (POST .../resume): a manager at its site, on a `suspended` permit, whose token holds the wide `write:safety_hub`. Stays true while the contractor has `certificate_gaps` — the resume then needs `proceed_anyway`, `certificate_override_reason` and `confirmed_gap_keys` (a 422 with `confirmation_required: true` until they are sent): a prompt, not a refusal."
          },
          "can_confirm_certificate_cover": {
            "type": "boolean",
            "description": "The caller may record \"Cover confirmed\" (POST .../confirm_certificate_cover): a manager at the permit's site whose token holds the wide `write:safety_hub`, on an issued permit whose contractor has certificate gaps nobody has confirmed yet."
          }
        }
      },
      "SafetyHubPermitCertificateGap": {
        "type": "object",
        "description": "One contractor certificate problem for a permit's work window\n(Vendors::Certification.on_site_gaps). Only certificates ticked\n\"Required for site work\" count; each is judged on its own, and a\nrenewal with the same type and title replaces an older one.\n",
        "properties": {
          "key": {
            "type": "string",
            "example": "cert:42",
            "description": "Stable identity of the gap — `cert:<certification_id>`, or `missing:insurance`. Send the keys you showed the user as `confirmed_gap_keys`."
          },
          "certification_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null for `missing`."
          },
          "cert_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "soc2",
              "coi",
              "w9",
              "security_audit",
              "insurance",
              "other"
            ],
            "description": "Null for `missing`."
          },
          "label": {
            "type": "string",
            "example": "Certificate of insurance"
          },
          "state": {
            "type": "string",
            "enum": [
              "expired",
              "expires_during_work",
              "not_yet_effective",
              "rejected",
              "missing"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-09-22",
            "description": "The calendar date the cover ends — the contractor pre-qualification's insurance date (`missing:prequalification_insurance`), or a certificate's recorded expiry day; null only when there is no expiry (a missing certificate). Print it as is — `expires_at` is an instant and can read as another day in a zone away from the business's."
          },
          "effective_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The certificate's issue (effective) date."
          },
          "replacement_pending_review": {
            "type": "boolean",
            "description": "A vendor-portal upload that would cover the window is awaiting review in Contracts (it is never counted as cover)."
          },
          "summary": {
            "type": "string",
            "example": "Certificate of insurance expired Sep 15, 2026"
          }
        }
      },
      "SafetyHubPermitCertificateConfirmation": {
        "type": "object",
        "description": "The \"proceed anyway\" a permit move needs while the contractor has\ncertificate gaps (CTR-030). Ignored when there are none.\n",
        "properties": {
          "proceed_anyway": {
            "type": "boolean",
            "description": "Required (true) when the contractor has certificate gaps."
          },
          "certificate_override_reason": {
            "type": "string",
            "maxLength": 2000,
            "description": "Required, non-blank, with `proceed_anyway`. Recorded on the permit."
          },
          "confirmed_gap_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The `key` of every gap you showed the user. A live gap missing from this list is refused (`certificate_gaps_changed: true`)."
          }
        }
      },
      "SafetyHubPermitCertificateGapRefusal": {
        "type": "object",
        "description": "422 body of a permit move refused over contractor certificate gaps (or\nany other 422 on that move, which carries only `error` and `status`).\n`error.code` is `validation_failed` and `error.message` a\nchannel-neutral sentence for display; the other fields say what to do.\n",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "validation_failed"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "status": {
            "type": "string"
          },
          "confirmation_required": {
            "type": "boolean",
            "description": "True — resend with `proceed_anyway`, a reason and the `confirmed_gap_keys` below."
          },
          "certificate_gaps_changed": {
            "type": "boolean",
            "description": "True when a live gap was not among `confirmed_gap_keys` — show the new list before asking again."
          },
          "certificate_gaps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SafetyHubPermitCertificateGap"
            }
          }
        }
      },
      "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"
          },
          "confidential": {
            "type": "boolean",
            "description": "Whether the incident is marked Confidential (the badge its detail\npage draws). Always `false` for an observation.\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,
                      "description": "Who made the change; null for an automatic change (see `automatic`).",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 2
                        },
                        "name": {
                          "type": "string",
                          "example": "Manager Name"
                        }
                      }
                    },
                    "automatic": {
                      "type": "boolean",
                      "description": "True for a change no person made — hours recounted after a holiday or working-day change, or approved leave cancelled when the employee left.",
                      "example": false
                    },
                    "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
              },
              "special_approval_requests": {
                "type": "integer",
                "example": 1
              },
              "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
                },
                "special_approval_requests": {
                  "type": "integer",
                  "example": 1
                },
                "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",
            "description": "Premium hours, double time INCLUDED — overtime_only_hours plus double_time_hours.",
            "example": 0.0
          },
          "overtime_only_hours": {
            "type": "number",
            "format": "float",
            "description": "Overtime hours excluding double time (the split /pay_information reports).",
            "example": 0.0
          },
          "double_time_hours": {
            "type": "number",
            "format": "float",
            "description": "Double-time hours, also counted in overtime_hours.",
            "example": 0.0
          },
          "submission_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-22T09:00:00Z"
          },
          "auto_submitted": {
            "type": "boolean",
            "description": "True while the submission that stands was made by timesheet auto-submit when the due date passed — the hours went in as recorded and the employee did not certify them.",
            "example": false
          },
          "auto_submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When timesheet auto-submit first submitted this sheet. Kept after a recall; a sheet is never auto-submitted twice.",
            "example": null
          },
          "approval_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2024-01-22T14:30:00Z"
          },
          "rejection_reason": {
            "type": "string",
            "nullable": true,
            "description": "What the approver wrote when sending the timesheet back (its rejection reason, else the manager's notes) — the text the employee is shown. Null unless the timesheet is rejected.",
            "example": "Missing Tuesday's clock-out"
          },
          "returned_for_correction": {
            "type": "boolean",
            "description": "True when the timesheet is rejected and that rejection was a return to the employee for corrections (not an outright rejection).",
            "example": false
          },
          "correction_requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the employee asked for a correction to this approved timesheet. Null when no request is open; returning the timesheet for correction clears it.",
            "example": null
          },
          "correction_request_reason": {
            "type": "string",
            "nullable": true,
            "description": "What the employee wrote when asking for the correction. Null when no request is open.",
            "example": null
          },
          "can_request_correction": {
            "type": "boolean",
            "description": "Whether the caller may send a correction request on this timesheet\n(POST /timesheets/{id}/request_correction): their own approved\ntimesheet, not managed in an external system.\n",
            "example": false
          },
          "can_answer_correction": {
            "type": "boolean",
            "description": "Whether the caller may answer the open correction request\n(POST /timesheets/{id}/close_correction): an approver of the employee\nwho is not the employee or one of their reports.\n",
            "example": false
          },
          "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
          },
          "source": {
            "$ref": "#/components/schemas/ManagedSource"
          },
          "entries_count": {
            "type": "integer",
            "description": "Number of entries in this timesheet",
            "example": 5
          },
          "missing_punches_count": {
            "type": "integer",
            "description": "Number of entries that still need time entered (missing a clock-in or clock-out). Absences are not counted.",
            "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"
              },
              "can_return_for_correction": {
                "type": "boolean",
                "description": "Whether the caller may send this approved timesheet back to its\nemployee (POST /timesheets/{id}/return_for_correction). False once\nits hours have been sent, or are being sent, to payroll.\n",
                "example": false
              },
              "correction_request_explainer": {
                "type": "string",
                "nullable": true,
                "description": "The sentence the web and phone show above the correction request\nbox — what a request does for this timesheet, including when its\nhours are already with payroll. Null when the caller cannot\nrequest a correction.\n",
                "example": "This timesheet is approved, so it can no longer be edited directly. Describe what's wrong and your approver will be told; they can send it back to you to fix and resubmit."
              }
            }
          }
        ]
      },
      "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 past entry still needs a clock-in or clock-out entered. False for an absence (see absent).",
            "example": false
          },
          "absent": {
            "type": "boolean",
            "description": "Whether this entry is an absence — a scheduled day with no times and no hours — which has nothing to enter.",
            "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",
            "description": "Premium hours, double time INCLUDED — overtime_only_hours plus double_time_hours.",
            "example": 0.0
          },
          "overtime_only_hours": {
            "type": "number",
            "format": "float",
            "description": "Overtime hours excluding double time.",
            "example": 0.0
          },
          "double_time_hours": {
            "type": "number",
            "format": "float",
            "description": "Double-time hours, also counted in overtime_hours.",
            "example": 0.0
          },
          "total_entries": {
            "type": "integer",
            "example": 5
          },
          "missing_punches": {
            "type": "integer",
            "description": "Entries that still need time entered. Absences are not counted.",
            "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",
                "nullable": true,
                "description": "Null when the entry has no hours. Pay is at the caller's own rate on file (hourly rate, or annual salary / 2080); when no rate is on file the hours are still given and every pay and rate field is null.",
                "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",
                    "nullable": true,
                    "example": 200.0
                  },
                  "overtime_pay": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "example": 0.0
                  },
                  "total_pay": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "example": 200.0
                  },
                  "hourly_rate": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "example": 25.0
                  },
                  "overtime_rate": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "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
          },
          "proficiency_text": {
            "type": "string",
            "description": "The level's name as the organization calls it (Skills & Credentials › Settings › Proficiency Levels), the platform's default names where it renamed none. Sent on every item of the list, bulk and expiring endpoints as well as show.",
            "example": "Intermediate"
          },
          "certification_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2024-01-15"
          },
          "expiration_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2025-01-15"
          },
          "expiring_soon": {
            "type": "boolean",
            "description": "True when the expiration date falls between today and the organization's alert lead time for expiring credentials (Skills & Credentials › Settings › Notifications, \"Credential expiring soon\"). False with no expiration date or once expired.",
            "example": true
          },
          "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"
          },
          "pending_upload": {
            "type": "boolean",
            "description": "A new certificate the employee uploaded while this credential is still valid, waiting for a manager's review. The credential itself stays verified and live meanwhile, so `verification_status=pending` can return rows with `verified: true`.",
            "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"
          },
          "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"
              }
            }
          },
          "detected": {
            "type": "boolean",
            "description": "Automatic skill detection added it (`source` is \"inferred\"); the web and phone mark it Detected.",
            "example": true
          },
          "source": {
            "type": "string",
            "description": "How the holding was added, as stored (\"inferred\", \"manual\", \"self_reported\", \"external\", …).",
            "example": "inferred"
          },
          "disputed": {
            "type": "boolean",
            "description": "Only on the caller's own holdings. They said \"Not mine\" and a reviewer has not decided yet — the holding is then inactive and unverified, which otherwise reads like a rejection.",
            "example": false
          },
          "not_mine_available": {
            "type": "boolean",
            "description": "Only on the caller's own holdings. POST /employee_skills/{id}/not_mine would take it off now: detected, active, its detection disputable, Skill Reviews (Competency Assessment) open, and a token that may write skills (`write:skills_certifications`, its own form included).",
            "example": true
          }
        }
      },
      "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
              },
              "verification_status": {
                "type": "string",
                "description": "`ended`: a manager took it off the record; it is kept as history and held on no date from then on.",
                "enum": [
                  "verified",
                  "pending",
                  "unverified",
                  "ended"
                ],
                "example": "verified"
              },
              "current_certification": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CertificationPeriod"
                  }
                ]
              },
              "certification_history": {
                "type": "array",
                "description": "Every award of this credential, newest first: each course, learning path, renewal or upload that awarded it is its own period.",
                "items": {
                  "$ref": "#/components/schemas/CertificationPeriod"
                }
              }
            }
          }
        ]
      },
      "CertificationPeriod": {
        "type": "object",
        "description": "One award of a credential and the dates it was valid.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 9876
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "superseded",
              "expired",
              "revoked",
              "declined",
              "pending",
              "rejected"
            ],
            "description": "active = the credential in force; superseded = replaced by a renewal or a later or longer award (history); pending = an upload waiting for review while a valid credential is held; rejected = an upload a reviewer refused; declined = a claim a reviewer refused.",
            "example": "active"
          },
          "status_label": {
            "type": "string",
            "description": "The status in the words the web app shows (Active, Superseded, Expired, Revoked, Declined, Pending verification).",
            "example": "Superseded"
          },
          "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"
          },
          "jurisdiction": {
            "type": "string",
            "nullable": true,
            "description": "The jurisdiction (state or province) that issued a licence.",
            "example": "California"
          },
          "license_compact": {
            "type": "string",
            "nullable": true,
            "enum": [
              "nlc"
            ],
            "description": "The compact a multistate licence is issued under (`nlc`, the Nurse Licensure Compact); null when single-state."
          },
          "ended_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The day this period stopped counting before its own expiry date — the credential was ended or the issuer revoked it.",
            "example": "2026-09-01"
          },
          "revoked_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the issuer revoked it, as recorded."
          },
          "issuing_authority": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true,
                "example": 12
              },
              "name": {
                "type": "string",
                "example": "State Licensing Board"
              }
            }
          },
          "credential_url": {
            "type": "string",
            "nullable": true
          },
          "verification_link": {
            "type": "string",
            "nullable": true
          },
          "ce_credits": {
            "type": "number",
            "nullable": true
          },
          "source": {
            "type": "string",
            "nullable": true,
            "example": "training"
          },
          "awarded_by_label": {
            "type": "string",
            "description": "What awarded this period, in words (a course, a learning path, an upload, a renewal).",
            "example": "Course: Forklift Safety"
          },
          "source_course": {
            "type": "object",
            "nullable": true,
            "description": "The course whose completion awarded this period.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 321
              },
              "title": {
                "type": "string",
                "example": "Forklift Safety"
              }
            }
          },
          "source_learning_path": {
            "type": "object",
            "nullable": true,
            "description": "The learning path whose completion awarded this period.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 55
              },
              "title": {
                "type": "string",
                "example": "Warehouse Onboarding"
              }
            }
          },
          "renewed_from_id": {
            "type": "integer",
            "nullable": true,
            "description": "The period this award replaced."
          },
          "verified": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "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",
                "description": "`ended`: a manager took it off the record; kept as history.",
                "enum": [
                  "active",
                  "expiring_soon",
                  "expiring_notice",
                  "expired",
                  "ended"
                ],
                "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
                    }
                  }
                }
              },
              "passwordless": {
                "type": "object",
                "description": "Passwordless sign-in through `POST /auth/passwordless/request`. Always disabled:\nthat endpoint delivers no link or code and answers `403 feature_disabled`, so a\nclient must not offer it. The keys are kept so existing decoders still read the block.\n",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "example": false
                  },
                  "methods": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "magic_link",
                        "sms_code"
                      ]
                    },
                    "example": []
                  },
                  "magic_link": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "example": false
                      },
                      "expires_in_minutes": {
                        "type": "integer",
                        "example": 15
                      }
                    }
                  },
                  "sms_code": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "example": false
                      },
                      "expires_in_minutes": {
                        "type": "integer",
                        "example": 5
                      }
                    }
                  }
                }
              }
            }
          },
          "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
                  }
                }
              }
            }
          },
          "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": {
            "type": "object",
            "description": "The person who signed in (Api::V1::Auth::SsoController#user_response).",
            "properties": {
              "id": {
                "type": "integer"
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "first_name": {
                "type": "string",
                "nullable": true
              },
              "last_name": {
                "type": "string",
                "nullable": true
              },
              "full_name": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "nullable": true
              },
              "timezone": {
                "type": "string",
                "description": "The zone they read — their own choice, else their place's, else\nthe company's — as stored: usually a Rails name, sometimes an IANA id.\n",
                "example": "Central Time (US & Canada)"
              },
              "time_zone_iana": {
                "type": "string",
                "nullable": true,
                "description": "The same zone as a canonical IANA id, for Intl and moment-timezone,\nwhich know no Rails name. Null only when the zone can't be resolved.\n",
                "example": "America/Chicago"
              }
            }
          },
          "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"
          },
          "language": {
            "type": "string",
            "description": "The language content reaches this person in — their own setting,\nelse their place's language, else the company default. Never null\n(Knowledge Layer P4); `language_set` says whether they chose it.\n",
            "example": "en"
          },
          "language_set": {
            "type": "boolean",
            "description": "Whether the person chose a language themselves. `false` is the\nprofile's \"Not set\": `language` then follows their place, else the\ncompany default.\n",
            "example": false
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "The zone the person reads — their own choice, else their place's,\nelse the company's — as stored: usually a Rails name, sometimes an\nIANA id.\n",
            "example": "America/New_York"
          },
          "time_zone_iana": {
            "type": "string",
            "nullable": true,
            "description": "The same zone as a canonical IANA id, for Intl and moment-timezone,\nwhich know no Rails name. Null only when the zone can't be resolved.\n",
            "example": "America/New_York"
          },
          "timezone_set": {
            "type": "boolean",
            "description": "Whether the person chose a zone themselves."
          },
          "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"
          }
        }
      },
      "WaitingOnYouAcknowledgeItem": {
        "type": "object",
        "description": "One \"To acknowledge\" row of Waiting on you — something the caller was\nasked to confirm and has not (Comms::Obligations::Item#to_payload).\n`asked_at` is when THIS person was asked (for a must-read post or a\nSites page, the day they joined its audience — a new hire added to an\nold post was asked the day they were added); `due_at` is the thing's\nown deadline. `asked_at` later than `due_at` means the person was asked\nonly after the deadline had passed.\n",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "must_read_post",
              "broadcast",
              "alert",
              "site_page",
              "policy",
              "policy_campaign",
              "sop",
              "job_description",
              "execution_post",
              "chat_message"
            ]
          },
          "id": {
            "type": "integer",
            "description": "The post, broadcast, alert, page, policy, campaign package, SOP, job description, execution post or chat message id"
          },
          "title": {
            "type": "string"
          },
          "kind_label": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The /m/ page where this is confirmed"
          },
          "asked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean"
          },
          "action_label": {
            "type": "string"
          },
          "open_label": {
            "type": "string",
            "description": "The words for a button that OPENS the item — \"Read and acknowledge\" for a must-read post or broadcast (acknowledged on its own page, after it is read), else `action_label`"
          },
          "confirm_in_chat": {
            "type": "boolean",
            "description": "Whether Ask AI's confirm_item can confirm this kind (must-read posts and broadcasts only)"
          }
        }
      },
      "WaitingOnYouDecideRow": {
        "type": "object",
        "description": "One \"To decide\" row of Waiting on you (Knowledge Layer E2) — a request\nor suggestion whose decision is the caller's, from the approvals\nregistry. Decided with `POST /api/v1/home/waiting_on_you/decide`\n(`source`, `id`; Knowledge Layer C3) or where `url` links;\n`can_approve` / `can_decline` say which verbs will be taken right now —\nfrom an API token, what that decide door takes from it: neither without\n`write:approvals`, and no delete-level verdict without its area's delete\nscope (`GET /home` and the clock-in brief alike).\n",
        "properties": {
          "source": {
            "type": "string",
            "description": "The approvals source (queue) key.",
            "example": "leave_requests"
          },
          "id": {
            "type": "integer",
            "description": "The record's id in that source."
          },
          "kind_label": {
            "type": "string",
            "example": "Time off"
          },
          "title": {
            "type": "string",
            "description": "Subject and summary as one line.",
            "example": "Ana Morales — Vacation · 9 Oct – 10 Oct"
          },
          "subtitle": {
            "type": "string",
            "nullable": true
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "urgency": {
            "type": "string",
            "enum": [
              "overdue",
              "soon",
              "normal"
            ]
          },
          "urgency_label": {
            "type": "string",
            "nullable": true,
            "description": "The words for `urgency` (\"Overdue\", \"Due soon\", or the source's own); null for `normal`."
          },
          "approve_label": {
            "type": "string",
            "example": "Approve"
          },
          "decline_label": {
            "type": "string",
            "example": "Decline"
          },
          "can_approve": {
            "type": "boolean"
          },
          "can_decline": {
            "type": "boolean"
          },
          "decline_needs_reason": {
            "type": "boolean"
          },
          "approve_question": {
            "type": "string",
            "nullable": true,
            "description": "A question to ask the person before sending `approve` (for example when they are approving their own request); null when Approve asks nothing."
          },
          "decline_question": {
            "type": "string",
            "nullable": true,
            "description": "A question to ask before sending `decline` (a refusal that takes no reason and cannot be undone); null when Decline asks nothing."
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Where it is decided (the /m/ page on the API)."
          }
        }
      },
      "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. A revoked session reads revoked, not expired (revoking also sets its expiry).",
            "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. Revoking a token also sets this to the time it was revoked.",
            "example": "2024-12-31T23:59:59Z"
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the token was revoked (by its owner, an admin, sign-out or rotation); null if it was never revoked",
            "example": null
          },
          "expired": {
            "type": "boolean",
            "description": "Whether the token has expired. A revoked token reads true here too; use revoked to tell the two apart.",
            "example": false
          },
          "revoked": {
            "type": "boolean",
            "description": "Whether the token was revoked. Revocation is permanent; a revoked token never becomes active again.",
            "example": false
          },
          "active": {
            "type": "boolean",
            "description": "Whether the token can be used (neither expired nor revoked)",
            "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": "The language content reaches the user in — their own setting, else\ntheir place's language, else the company default (Knowledge Layer\nP4; was the stored setting or \"en\"). Never null.\n",
            "example": "en"
          },
          "language_set": {
            "type": "boolean",
            "description": "Whether the user chose a language themselves. `false` is the\nprofile's \"Not set\" — `preferred_language` then follows their place\nor the company default.\n",
            "example": true
          },
          "timezone": {
            "type": "string",
            "description": "The zone the user reads — their own choice, else their place's, else\nthe company's — as stored: usually a Rails name, sometimes an IANA id.\n",
            "example": "America/New_York"
          },
          "time_zone_iana": {
            "type": "string",
            "nullable": true,
            "description": "The same zone as a canonical IANA id, for Intl and moment-timezone,\nwhich know no Rails name. Null only when the zone can't be resolved.\nRead-only: a PATCH sets the zone through `timezone`.\n",
            "example": "America/New_York"
          },
          "timezone_set": {
            "type": "boolean",
            "description": "Whether the user chose a zone themselves (`false` — `timezone` follows their place or the company)."
          },
          "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",
            "nullable": true,
            "description": "The person's own language. `null` or blank clears it — the profile's\n\"Not set\": content then follows their place's language (Knowledge\nLayer P4). Sending back the value GET returned for someone who never\nchose one (`language_set: false`) changes nothing: it is their\nplace's language, not a choice.\n"
          },
          "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`.\nWhile the inspection is `in_progress`, the reason it was returned\n(`Inspection#last_return_feedback` — the newer of a reviewer's\nrejection and a Frontline campaign return), or null if it never was.\n"
          },
          "returned_for_rework": {
            "type": "boolean",
            "description": "Whether this in-progress inspection was sent back for rework, by a\nreviewer or a campaign reviewer. The detail payload carries the\nreason in `return_feedback`.\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\"Step N\" when the step has no name of its own. 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
          },
          "instructions": {
            "type": "string",
            "nullable": true,
            "description": "The template author's instructions to the inspector, drawn under the description."
          },
          "requires_photo": {
            "type": "boolean"
          },
          "requires_note": {
            "type": "boolean",
            "example": false,
            "description": "The template asks a note on this item; completing refuses an answered item without one (422 `note_required`)."
          },
          "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"
          },
          "standard_text": {
            "type": "string",
            "nullable": true,
            "description": "The item's written photo standard — what a photo of it should show."
          },
          "reference_image_url": {
            "type": "string",
            "nullable": true,
            "description": "The photo standard's reference picture, as a signed link valid for\nan hour: show it, never store it — fetch the inspection again for a\nfresh one. Null when the item has none (or its reference was archived).\n"
          }
        }
      },
      "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"
          },
          "display_answer": {
            "type": "string",
            "nullable": true,
            "example": "45 °F",
            "description": "The answer as the web reports print it — option labels, a number with its unit, a formatted time. Null for yes/no, checkbox, photo, signature and instruction items, and when unanswered."
          },
          "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"
          },
          "server_version": {
            "type": "string",
            "description": "updated_at at microsecond precision (opaque). Echo as `base_version` on sync."
          },
          "template_item": {
            "$ref": "#/components/schemas/InspectionItemTemplateSnapshot",
            "nullable": true
          },
          "photo_required": {
            "type": "boolean",
            "example": true,
            "description": "The item must carry a photo before the inspection can be submitted — its template sets `requires_photo`, or it is a required photo-answer item. The rule the submit enforces; false once the item is marked N/A or skipped."
          },
          "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
          },
          "ai_photo_check": {
            "type": "object",
            "nullable": true,
            "description": "The AI photo check's verdict on the item's photos, against the\nchecklist item's photo standard (a reference picture and/or a written\nstandard): the worst verdict among the photos still attached. A\nsuggestion the evaluator confirms; null when the checklist item has no\nstandard or no attached photo has been checked. A verdict given while\nthe check was on stays after it is turned off.\nA check that could not run (an unreadable photo, no answer from the\nmodel, an AI usage limit, an error) reads verdict `unchecked` with\nits `reason`; attaching the photo again checks it again.\n",
            "properties": {
              "verdict": {
                "type": "string",
                "enum": [
                  "matches",
                  "deviation",
                  "unclear",
                  "unchecked"
                ]
              },
              "summary": {
                "type": "string"
              },
              "reason": {
                "type": "string",
                "enum": [
                  "unreadable",
                  "no_response",
                  "ai_blocked",
                  "error",
                  "no_standard"
                ],
                "description": "Only on an `unchecked` verdict: why the check did not run."
              },
              "media_item_id": {
                "type": "integer"
              },
              "checked_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "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 — the person's answer. Gate the Approve affordance on\n`can_approve` and Reject on `can_reject` (server-authoritative), not\non client-side role inference.\n",
            "example": true
          },
          "can_approve": {
            "type": "boolean",
            "description": "`can_review` AND the caller's token holds `write:inspections`,\nwhich the review_approve endpoint asks. A read-only token may read\nthe review but its approve answers 403. A session always passes the\nscope half.\n",
            "example": true
          },
          "can_reject": {
            "type": "boolean",
            "description": "`can_review` AND the caller's token holds `destructive:inspections`,\nwhich the review_reject endpoint asks (a rejection is a final act).\nA write-only token may approve but its reject answers 403. A\nsession always passes the scope half.\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"
            ]
          },
          "status_label": {
            "type": "string",
            "enum": [
              "Not started",
              "In progress",
              "Completed",
              "Cancelled"
            ],
            "description": "The word every screen shows for `status` (Capa::Action#status_label):\n`pending` is \"Not started\", never \"Pending\". An action reads\n\"In progress\" only once someone starts the work — assigning it\nnever moves the status. Show this; keep `status` for logic.\n"
          },
          "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
          },
          "auto_assignment_rule": {
            "type": "string",
            "nullable": true,
            "enum": [
              "location_manager",
              "failure_group",
              "app_admin",
              "admin",
              null
            ],
            "description": "Which tier of the Inspections owner rule chose `assigned_to` when\nthe app assigned the action on its own (a failed item at\ncompletion, a failure-prompt action adopted at completion, a rack\nescalation): a manager of the inspection's location, a member of\nthe failure alert group, an Inspections app admin, or an\norganization admin — the first of these who can open the\ninspection. `null` when a person chose the owner, or the action\nhas been reassigned since.\n"
          },
          "auto_assignment_reason": {
            "type": "string",
            "nullable": true,
            "example": "Assigned automatically as a manager at Corporate Office.",
            "description": "The sentence every screen shows beside the assignee for an\naction the app assigned (Capa::Action#auto_assignment_reason).\n`null` exactly when `auto_assignment_rule` is.\n"
          },
          "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"
          }
        }
      },
      "InspectionCorrectiveActionHandoff": {
        "type": "object",
        "description": "Present only when the completion this response reports created\ncorrective actions or gave failure-prompt actions an owner: who they\nwent to, read from the stored rows. Each was assigned to the first\nperson who can open the inspection — a manager of its location, then a\nmember of the failure alert group, then an Inspections app admin, then\nan organization admin (see `auto_assignment_rule` on each action).\n",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Actions created or handed an owner by this completion."
          },
          "assigned": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "user_id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "unassigned_count": {
            "type": "integer",
            "description": "Actions nobody could be assigned automatically."
          },
          "message": {
            "type": "string",
            "example": "2 corrective actions created and assigned to Rosa Delgado.",
            "description": "The sentence the web Complete notice shows, in the caller's language."
          }
        }
      },
      "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,
                "description": "Seconds from start to hand-in as stored at hand-in; nil on rows from before it was stored. Prefer start_to_finish_seconds."
              },
              "start_to_finish_seconds": {
                "type": "integer",
                "nullable": true,
                "description": "Start to finish — elapsed seconds from start to hand-in (submission for review, else completion), the figure every screen shows. Not hands-on time."
              },
              "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
              },
              "return_feedback": {
                "type": "object",
                "nullable": true,
                "description": "Why this in-progress inspection was sent back for rework — the\nnewer of a reviewer's rejection and a Frontline campaign return.\nNull unless `returned_for_rework`.\n",
                "properties": {
                  "comment": {
                    "type": "string",
                    "nullable": true
                  },
                  "by": {
                    "type": "string",
                    "nullable": true,
                    "description": "The reviewer's name, when known."
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "asset": {
                "$ref": "#/components/schemas/InspectionAsset",
                "nullable": true
              },
              "previous_visit": {
                "type": "object",
                "description": "The same location's previous completed inspection on this\nchecklist and how items changed since it. `available` is false\nfor a first visit or an inspection with nothing to compare by (no\nlocation, asset or facility layout). Visits are ordered by when\nthey were handed in. On an open inspection the only item change\nis `failed_last_visit`; on a completed one, `failed_again`,\n`newly_failed` or `fixed`.\n",
                "properties": {
                  "available": {
                    "type": "boolean"
                  },
                  "previous_inspection_id": {
                    "type": "integer"
                  },
                  "previous_completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When the previous visit was completed — its approval time on a review-gated checklist."
                  },
                  "previous_visited_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When the previous visit happened (handed in by its inspector). Date a visit by this."
                  },
                  "previous_score": {
                    "type": "number",
                    "nullable": true
                  },
                  "score_change": {
                    "type": "number",
                    "nullable": true
                  },
                  "counts": {
                    "type": "object",
                    "properties": {
                      "failed_again": {
                        "type": "integer"
                      },
                      "newly_failed": {
                        "type": "integer"
                      },
                      "fixed": {
                        "type": "integer"
                      }
                    }
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "inspection_item_id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "change": {
                          "type": "string",
                          "enum": [
                            "failed_again",
                            "newly_failed",
                            "fixed",
                            "failed_last_visit"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "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"
              },
              "can_reject": {
                "type": "boolean",
                "description": "`can_review` AND the caller's token holds\n`destructive:inspections`, which the review_reject endpoint asks.\nDraw Reject off this flag and Approve off `can_approve`.\n"
              },
              "can_approve": {
                "type": "boolean",
                "description": "`can_review` AND the caller's token holds `write:inspections`,\nwhich the review_approve endpoint asks — a read-only token may\nread the review but its approve answers 403.\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"
              },
              "server_version": {
                "type": "string",
                "description": "The inspection's updated_at at microsecond precision (opaque).\nEcho as `inspection.base_version` on sync; guards notes /\nsummary / gps_* only.\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",
            "description": "Items an inspection of this template asks — the active ones. An\nitem switched off in the builder is not counted (the detail\npayload's `items` leaves it out too).\n",
            "example": 13
          },
          "inspections_count": {
            "type": "integer",
            "description": "The template's counter of inspections in every status. On a shared\nsystem template (`include_system=true`) it counts every\norganization's. For this organization's completed inspections read\n`completed_inspections_count`.\n",
            "example": 142
          },
          "completed_inspections_count": {
            "type": "integer",
            "description": "This organization's completed inspections of the template. Present\non the template list and the template detail; absent where a\npayload does not resolve it (the offline bundle manifest).\n",
            "example": 97
          },
          "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
          },
          "instructions": {
            "type": "string",
            "nullable": true,
            "description": "The template author's instructions to the inspector, drawn under the description."
          },
          "item_type": {
            "$ref": "#/components/schemas/InspectionItemType"
          },
          "weight": {
            "type": "number",
            "example": 1.0
          },
          "critical": {
            "type": "boolean",
            "example": false
          },
          "requires_photo": {
            "type": "boolean",
            "example": false
          },
          "requires_note": {
            "type": "boolean",
            "example": false,
            "description": "The template asks a note on this item; completing refuses an answered item without one (422 `note_required`)."
          },
          "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 an unknown priority/status all 422 the whole sync / complete\nrequest (atomic — no partial writes). A status the dedicated endpoints\nwill not write is different: the entry is skipped whole, the rest of\nthe request saves, and the response lists it in\n`skipped_corrective_actions` (#6883).\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",
            "description": "`completed` stamps the completion date. `cancelled` is never written, and a completed or cancelled action's status does not change (`completed` sent again for a completed action is a no-op): such an entry is skipped and listed in `skipped_corrective_actions`."
          },
          "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
          }
        }
      },
      "InspectionSkippedCorrectiveActions": {
        "type": "array",
        "description": "Present only when a `corrective_actions` entry was skipped: its status is one the dedicated corrective-action endpoints will not write (`cancelled`, or a change to a completed or cancelled action), or it asked to complete an action the company requires a photo for (`completion_photo_required`). Nothing in a skipped update entry was saved; a skipped create entry was added open (its `id` is the new action). Everything else in the request was saved.",
        "items": {
          "type": "object",
          "required": [
            "index",
            "status",
            "reason",
            "message"
          ],
          "properties": {
            "index": {
              "type": "integer",
              "description": "Position of the entry in the request's `corrective_actions` array."
            },
            "id": {
              "type": "integer",
              "nullable": true,
              "description": "The corrective action: for an update entry, or a create entry added open; otherwise null."
            },
            "status": {
              "type": "string",
              "example": "cancelled",
              "description": "The status the entry asked for."
            },
            "reason": {
              "type": "string",
              "enum": [
                "status_not_writable",
                "completion_photo_required"
              ]
            },
            "message": {
              "type": "string",
              "example": "This corrective action wasn't changed: it can't be set to cancelled in the app."
            }
          }
        }
      },
      "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. An entry\nwhose status the dedicated endpoints will not write is skipped and\nlisted in `skipped_corrective_actions`.\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": "Accepted and IGNORED — a device clock, not comparable with server timestamps. For conflict detection send `base_version` on the inspection and on each item."
          },
          "inspection": {
            "$ref": "#/components/schemas/InspectionSyncRequestInspection"
          },
          "items": {
            "type": "array",
            "description": "Item updates to apply (located by `id` or `template_item_id`).",
            "items": {
              "$ref": "#/components/schemas/InspectionSyncRequestItem"
            }
          },
          "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); an entry whose status the dedicated endpoints will not\nwrite is skipped and listed in `skipped_corrective_actions`. When\nthe sync also carries `complete: true`, CAs land BEFORE `submit!`\nso the post-completion fan-out skips creating duplicates for the\nsame failed items.\n",
            "items": {
              "$ref": "#/components/schemas/InspectionCorrectiveActionCreateItem"
            }
          },
          "complete": {
            "type": "boolean",
            "default": false,
            "description": "When true, call `submit!` after applying the batch."
          }
        }
      },
      "InspectionSyncRequestItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InspectionCreateItem"
          },
          {
            "type": "object",
            "properties": {
              "base_version": {
                "type": "string",
                "nullable": true,
                "description": "Optional. The item's `server_version` from the bundle or a previous response. When the server row changed after it, this item is NOT applied and is listed in the response's `conflicts`. Absent = last-write-wins. Not an ISO-8601 string = 422 `invalid_base_version`."
              }
            }
          }
        ]
      },
      "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
          },
          "base_version": {
            "type": "string",
            "nullable": true,
            "description": "Optional. The inspection's `server_version`. When stale, notes / summary / gps_* are NOT applied (item answers are guarded per item) and the inspection is listed in the response's `conflicts`."
          }
        }
      },
      "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"
            ],
            "description": "`in_progress` on a Not started (`pending`) action is Start work: it\ngoes through Capa::Action#start!, which records who started it on\nthe action's trail. `completed` goes through Capa::Action#complete!.\n"
          },
          "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"
        ]
      },
      "InspectionMediaItem": {
        "type": "object",
        "description": "One attached photo or video. Source:\n`Api::V1::Inspections::ItemsController#media_item_payload`.\n",
        "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",
            "nullable": true
          },
          "thumb_url": {
            "type": "string",
            "nullable": true
          },
          "has_annotations": {
            "type": "boolean"
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "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
          },
          "anchor_month": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "maximum": 12,
            "description": "The month a quarterly or annual schedule is anchored to. Annually: the\nmonth it runs in. Quarterly: stored as 1, 2 or 3, meaning\nJan/Apr/Jul/Oct, Feb/May/Aug/Nov or Mar/Jun/Sep/Dec. Null for the\nother frequencies.\n"
          },
          "due_months": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "The months (1-12) a quarterly or annual schedule runs in; null for the other frequencies."
          },
          "recurrence": {
            "type": "string",
            "description": "The schedule page's description of when it runs, e.g. \"Quarterly on day 1 (Jan, Apr, Jul, Oct)\"."
          },
          "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"
          },
          "status_label": {
            "type": "string",
            "description": "The state's one word, as every web surface shows it (\"In progress\", \"Ready for sign-off\", \"Submitted for review\", \"Signed off\"), in the caller's language. \"Ready for sign-off\" is an `in_progress` sweep with every aisle covered or skipped (InspectionSweep#display_status) — `status` stays `in_progress` until it is 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
              }
            }
          },
          "handed_off_aisles": {
            "type": "array",
            "description": "Aisles started and left with nobody on them, in floor order — each can be picked up (POST claim_aisle with aisle_id).",
            "items": {
              "type": "object",
              "properties": {
                "aisle_id": {
                  "type": "integer"
                },
                "aisle_label": {
                  "type": "string"
                },
                "frontier_bay": {
                  "type": "integer",
                  "nullable": true
                },
                "handoff_note": {
                  "type": "string",
                  "nullable": true
                },
                "left_by": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "left_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                }
              }
            }
          },
          "next_step": {
            "type": "string",
            "nullable": true,
            "enum": [
              "continue",
              "resume",
              "pick_up",
              "start",
              "ready_for_sign_off",
              "awaiting_sign_off",
              "in_review",
              "signed_off",
              null
            ],
            "description": "What the caller does next on this sweep (InspectionSweep#next_step_for — the rule the web \"Coverage sweeps\" cards read). `continue`: the caller's own session is still open on an aisle not yet covered. `ready_for_sign_off` / `awaiting_sign_off`: every aisle is covered or skipped, and the caller may (or may not) sign it off — never `start` at 100%. `in_review`: sent for review. A token without write:inspections is never `ready_for_sign_off`."
          },
          "next_step_label": {
            "type": "string",
            "nullable": true,
            "description": "The line the web card draws for `next_step`, in the caller's language (e.g. \"Nothing left to sweep · Ready for sign-off\")."
          },
          "next_step_action_label": {
            "type": "string",
            "nullable": true,
            "description": "The button beside it, in the caller's language (Continue, Resume, Pick up, Start, Sign off, View). \"Sign off\" opens the sweep, whose sign-off is POST /inspections/sweeps/{id}/sign_off."
          },
          "next_step_aisle_id": {
            "type": "integer",
            "nullable": true,
            "description": "The aisle the step is about — the caller's own aisle (`continue`), or the hand-off to `resume` / `pick_up` (POST claim_aisle with this aisle_id). Null otherwise."
          },
          "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"
          },
          "ready_to_cover": {
            "type": "boolean",
            "description": "Every bay is resolved but the aisle is not marked covered yet — prompt \"Aisle N complete\" with Mark aisle covered (POST …/aisles/{id}/cover) and Next aisle. Never cover on the inspector's behalf."
          },
          "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_cover_aisles": {
                "type": "boolean",
                "description": "May mark an aisle covered (POST …/aisles/{id}/cover) — may write and the sweep is not signed off. Offer it on any aisle not covered or skipped."
              },
              "can_sign_off": {
                "type": "boolean",
                "description": "May sign off (SweepPolicy#can_sign_off? — the rule POST …/sign_off asks), not already signed off, and not already pending review. Open critical findings warn, they do not block."
              },
              "untracked_critical_count": {
                "type": "integer",
                "description": "Critical findings not yet resolved, voided, escalated or attached to a corrective action — the number the sign-off warning names."
              },
              "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)."
              },
              "notable_form_schema": {
                "type": "object",
                "description": "The notable-exception form — the cycle's notable template, or the built-in simple form. Carries `component_optional: true` and `component_default` (\"General\"): a notable finding may name no component (send none or blank, on create and on edit) and is filed under the default. Pre-select nothing, and let a chosen component be cleared."
              },
              "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 — the photo, or a generated initials image when there is none."
          },
          "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 — the photo, or a generated initials image when there is none."
          },
          "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."
          },
          "answers_removed_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The day a request about the subject's personal data removed what was said — a year after they left. The round still counts toward its leader's coverage; it has no answers or notes to show. Null on every other round.",
            "example": null
          },
          "private_notes": {
            "type": "string",
            "nullable": true,
            "description": "The round's OWN leader only, on this API. Absent from the payload entirely for everyone else — the subject, the leader's management chain and app admins (the web admits the chain; this door is deliberately narrower). See `private_notes_visible` on the detail response."
          },
          "answers_count": {
            "type": "integer",
            "description": "Answers recorded on the round. Emitted by GET /leader-rounds/rounds (batched for the page) and by the create and detail replies — not by /leader-rounds/my or the skip reply."
          },
          "issues_count": {
            "type": "integer",
            "description": "Issues the round raised onto the stoplight ledger. Emitted where `answers_count` is."
          },
          "substantive": {
            "type": "boolean",
            "description": "False for a completed round with every answer blank and no issue (the web's \"No answers\" badge — coverage does not count it, so the person stays due), and for a draft or a skip. On every round payload."
          },
          "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
          },
          "excludable_from_reporting": {
            "type": "boolean",
            "description": "Whether the leader may leave this question's answer out of reporting by sending `exclude_from_reporting: true` in its `answers` hash — text, scale, yes/no and choice questions. Drawn on the web and phone forms as \"Leave this answer out of themes and reports\".",
            "example": true
          },
          "exclude_from_reporting_label": {
            "type": "string",
            "nullable": true,
            "description": "The box's label, as the web and phone forms draw it; null when `excludable_from_reporting` is false.",
            "example": "Leave this answer out of themes and reports"
          },
          "exclude_from_reporting_help": {
            "type": "string",
            "nullable": true,
            "description": "The sentence the forms say once above the boxes — leaving an answer out does not hide it from anyone who can read the round; null when `excludable_from_reporting` is false.",
            "example": "Leaving an answer out of themes and reports doesn't hide it from anyone who can read the round."
          },
          "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 — \"4 of 5\" for a rating, \"Yes\"/\"No\" for boolean, the name for a recognition pick.",
            "example": "4 of 5"
          },
          "value_text": {
            "type": "string",
            "nullable": true
          },
          "value_number": {
            "type": "number",
            "nullable": true,
            "description": "The scale value."
          },
          "value_boolean": {
            "type": "boolean",
            "nullable": true
          },
          "excluded_from_reporting": {
            "type": "boolean",
            "description": "The leader left this answer out of reporting when they recorded the round: it is on the round, and in no theme, AI theme or engagement score.",
            "example": false
          },
          "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",
            "nullable": true,
            "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. Null for the person rounded on, like `recognition_status`: whether their leader's recognition of a colleague was posted is not theirs to see (it stays true after the post is rejected or removed).",
            "example": false
          },
          "recognition_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "active",
              "pending_approval",
              "pending_review",
              "rejected",
              "hidden",
              "deleted",
              null
            ],
            "description": "The posted recognition's own status — `pending_approval` while a manager still has it in their queue, `active` once it is live. Null when nothing was posted, when the post no longer exists, and always for the person the round was about (its moderation outcome is not theirs to see).",
            "example": null
          }
        }
      },
      "LeaderRoundsWarningDetail": {
        "type": "object",
        "description": "One save warning — the round or recognition saved, but part of the submission did not land as sent.",
        "required": [
          "code",
          "message",
          "question_id"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "payload_not_keyed",
              "issue_description_shortened",
              "recognition_pick_inactive",
              "issue_sent_as_answer",
              "issues_capped",
              "issue_owner_inactive",
              "due_date_too_far",
              "due_date_in_past",
              "due_date_unreadable",
              "recognition_marker_not_saved",
              "value_not_scalar",
              "warnings_not_replayed",
              "exclusion_not_supported"
            ],
            "example": "due_date_in_past"
          },
          "message": {
            "type": "string",
            "description": "The same English sentence `warnings` carries.",
            "example": "A due date of Sep 29, 2026 is in the past — it was set to today."
          },
          "question_id": {
            "type": "integer",
            "nullable": true,
            "description": "The template question the warning concerns, when it concerns one.",
            "example": 1234
          }
        }
      },
      "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"
          },
          "owner_active": {
            "type": "boolean",
            "description": "False when the owner has left the account (deactivated, or no active membership here) — the issue stays on the ledger with their name, so draw it as \"no longer active here\" rather than as work someone is chasing. True for an unassigned issue.",
            "example": true
          },
          "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
          }
        }
      },
      "AiFrontOfficeBookingRequest": {
        "type": "object",
        "description": "One booking request as the review queue lists it.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "example": "declined"
          },
          "source": {
            "type": "string"
          },
          "urgency": {
            "type": "string"
          },
          "service_type_key": {
            "type": "string",
            "nullable": true
          },
          "customer_name": {
            "type": "string",
            "nullable": true
          },
          "customer_phone": {
            "type": "string",
            "nullable": true
          },
          "customer_email": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "requested_window_start": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "requested_window_end": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "confidence_score": {
            "type": "number",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "first_contact_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "converted_work_order_id": {
            "type": "integer",
            "nullable": true
          },
          "assigned_dispatcher_id": {
            "type": "integer",
            "nullable": true
          },
          "sla_status": {
            "type": "string",
            "nullable": true,
            "description": "Null where no response countdown applies (a declined request has none)."
          }
        }
      },
      "AiInterviewRefusal": {
        "type": "object",
        "description": "A refusal, in the platform's envelope. `details.ai_interview` is the\nconversation as it stands, so a client redraws from the refusal\nitself. Codes: `conversation_closed` (409 — the app's door shut),\n`not_taking_turns` (409; on POST …/speak `details.transcript` carries\nwhat the clip said, as for `busy`), `busy` (409 — a reply is still being\nwritten; on POST …/speak `details.transcript` carries what the clip said, already\ntranscribed, so a client keeps the words), `reply_failed` (502 — the words are saved; POST …/retry),\n`ai_usage_blocked`, `not_reviewing` (409), `already_sent` (409),\n`not_deleted` (409), `unknown_language`, `audio_missing`, `no_speech`,\n`clip_too_large` (413), `voice_unavailable` (503),\n`recording_not_offered`, and the engine's own: `say_something`,\n`moved_on`, `choose_shown`, `needs_answer`, `write_something`,\n`write_an_answer`, `unknown_question`, `unknown_answer`,\n`unknown_verdict`, `quote_choice`, `choose_one`, `choose_some`,\n`felt_captured`, `nothing_to_change`; `private_conversation` (403 —\nan impersonation token, or a web session viewing as the person: a\nconversation is private to the person it belongs to, so it is never\nshown, started, answered or sent while acting as them;\n`details.ai_interview` is then only the list's line for it, with its\nstatus — GET /ai_interviews still lists them); `invalid` (422) for what could\nnot be saved, in the message the page would show; on a send,\n`missing_required` (with `details.missing`), `already_participated`\nand `closed` (409), `invalid`, `not_sent` (refused with no code of its\nown), or the app's own refusal under its code.\n",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "busy"
              },
              "message": {
                "type": "string",
                "example": "Still thinking about your last answer…"
              },
              "details": {
                "type": "object",
                "properties": {
                  "ai_interview": {
                    "$ref": "#/components/schemas/AiInterview"
                  },
                  "missing": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "On a refused send, the item keys still needing an answer."
                  }
                }
              }
            }
          }
        }
      },
      "AiInterviewResponse": {
        "type": "object",
        "properties": {
          "ai_interview": {
            "$ref": "#/components/schemas/AiInterview"
          }
        }
      },
      "AiInterviewSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "purpose": {
            "type": "string",
            "example": "Your exit interview"
          },
          "app": {
            "type": "string",
            "nullable": true,
            "description": "The app the conversation is for (its slug).",
            "example": "offboarding"
          },
          "status": {
            "type": "string",
            "enum": [
              "in_progress",
              "coding",
              "reviewing",
              "submitted"
            ]
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "submitted_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "web_url": {
            "type": "string",
            "description": "The conversation's page in a browser."
          }
        }
      },
      "AiInterview": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AiInterviewSummary"
          },
          {
            "type": "object",
            "properties": {
              "screen": {
                "type": "string",
                "enum": [
                  "opening",
                  "conversation",
                  "coding",
                  "review",
                  "done",
                  "closed"
                ],
                "description": "What the page shows: the opening (the consent), the conversation, coding (the review is being drafted — poll), the review, done, or closed — the app's own door shut on an unsent conversation, which may still be deleted."
              },
              "rehearsal": {
                "type": "boolean"
              },
              "language": {
                "type": "string",
                "example": "en"
              },
              "return_url": {
                "type": "string",
                "nullable": true,
                "description": "The app's own page Done leads to — another page the app names once its door has shut."
              },
              "closed_reason": {
                "type": "string",
                "nullable": true,
                "description": "On `screen: \"closed\"`, the app's own reason its door shut (conversations paused, the plan cancelled) — shown in place of the generic sentence."
              },
              "closed_alternative": {
                "type": "object",
                "nullable": true,
                "description": "On `screen: \"closed\"`, the app's own way to answer (a form) when its reason offers it — the form still takes the record.",
                "properties": {
                  "label": {
                    "type": "string",
                    "example": "Fill in the feedback form instead"
                  },
                  "web_url": {
                    "type": "string"
                  }
                }
              },
              "unsent_note": {
                "type": "string",
                "nullable": true,
                "description": "On the conversation, coding and review screens, how long an unsent conversation waits to be sent (or the app's own deadline)."
              },
              "about": {
                "type": "object",
                "description": "The conversation's answers about itself.",
                "properties": {
                  "who_sees": {
                    "type": "string"
                  },
                  "what_happens": {
                    "type": "string"
                  },
                  "reach_a_person": {
                    "type": "string"
                  },
                  "after_sending": {
                    "type": "string"
                  }
                }
              },
              "opening": {
                "$ref": "#/components/schemas/AiInterviewOpening"
              },
              "turns": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AiInterviewTurn"
                }
              },
              "progress": {
                "type": "array",
                "description": "Topic progress; the fixed questions fold into one \"Quick questions\" row.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "current",
                        "upcoming",
                        "covered",
                        "declined",
                        "not_reached",
                        "not_applicable",
                        "inferred"
                      ]
                    }
                  }
                }
              },
              "composer": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AiInterviewComposer"
                  }
                ],
                "nullable": true,
                "description": "On the conversation screen only."
              },
              "review": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AiInterviewReview"
                  }
                ],
                "nullable": true,
                "description": "On the review screen only."
              }
            }
          }
        ]
      },
      "AiInterviewOpening": {
        "type": "object",
        "description": "The opening screen — also the consent.",
        "properties": {
          "minutes": {
            "type": "integer",
            "example": 8
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "conditional_topics": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The topics (from `topics`) asked only when an earlier answer meets their condition — shown as depending on an earlier answer, never promised outright."
          },
          "quick_questions": {
            "type": "integer",
            "description": "How many fixed questions are answered by tapping."
          },
          "quick_questions_up_to": {
            "type": "boolean",
            "description": "True when some quick questions are asked only on certain earlier answers, so `quick_questions` is an upper bound (\"Up to N\")."
          },
          "facts": {
            "type": "array",
            "description": "What the interviewer was told about the person — shown to them.",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "example": "Role"
                },
                "value": {
                  "type": "string",
                  "example": "Line Lead"
                }
              }
            }
          },
          "some_required": {
            "type": "boolean",
            "description": "Some questions need an answer; any others may be skipped."
          },
          "required_questions": {
            "type": "integer",
            "example": 1,
            "description": "How many questions need an answer (0 when any may be skipped)."
          },
          "required_share": {
            "type": "string",
            "enum": [
              "none",
              "one",
              "few",
              "most",
              "all"
            ],
            "example": "most",
            "description": "How much of the conversation needs an answer, as the opening says it — none (any question may be skipped), one, a few, most (more than half of the questions it can ask) or all (none may be skipped)."
          },
          "alternative": {
            "type": "object",
            "nullable": true,
            "description": "The app's own way to answer instead (a form) — null while that form does not take the record (a shift past its feedback window).",
            "properties": {
              "label": {
                "type": "string",
                "example": "Fill in the form instead"
              },
              "web_url": {
                "type": "string"
              }
            }
          }
        }
      },
      "AiInterviewTurn": {
        "type": "object",
        "properties": {
          "sequence": {
            "type": "integer"
          },
          "role": {
            "type": "string",
            "enum": [
              "interviewer",
              "participant"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "opening",
              "question",
              "measure_prompt",
              "answer",
              "addition",
              "followup",
              "reflection",
              "notice",
              "sitting_end",
              "closing"
            ]
          },
          "text": {
            "type": "string"
          },
          "item_key": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AiInterviewComposer": {
        "type": "object",
        "properties": {
          "transports": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "text",
                "recording"
              ]
            },
            "description": "How an answer may be sent now (POST …/turn, …/speak)."
          },
          "awaiting_reply": {
            "type": "boolean",
            "description": "The person's last words have no reply — POST …/retry."
          },
          "between_sittings": {
            "type": "boolean",
            "description": "One topic per sitting — this one is done; POST …/begin starts the next."
          },
          "next_topic": {
            "type": "string",
            "nullable": true
          },
          "item": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AiInterviewItem"
              }
            ],
            "nullable": true,
            "description": "The question being asked now."
          }
        }
      },
      "AiInterviewItem": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "exploration",
              "measure"
            ]
          },
          "label": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "required": {
            "type": "boolean"
          },
          "can_skip": {
            "type": "boolean"
          },
          "scale": {
            "type": "string",
            "nullable": true,
            "description": "A measure's: nps, rating, boolean, single, multiselect …"
          },
          "multiple": {
            "type": "boolean"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "integer"
                    }
                  ]
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "min_label": {
            "type": "string",
            "nullable": true
          },
          "max_label": {
            "type": "string",
            "nullable": true
          },
          "clarification": {
            "type": "string",
            "nullable": true
          },
          "pending_value": {
            "description": "A typed answer waiting to be confirmed by a tap.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          }
        }
      },
      "AiInterviewReview": {
        "type": "object",
        "description": "\"Here's what we'll record.\"",
        "properties": {
          "missing": {
            "type": "array",
            "description": "Questions the app needs answered before it can be sent (PATCH …/review with `missing_item`).",
            "items": {
              "$ref": "#/components/schemas/AiInterviewItem"
            }
          },
          "topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiInterviewAnswer"
            }
          },
          "quick_answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiInterviewAnswer"
            }
          },
          "quote_choices": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "none",
                "deidentified",
                "with_name"
              ]
            }
          },
          "quote_question": {
            "type": "boolean",
            "description": "Whether to ask \"May we quote your words?\" — true only while an answer that will be recorded has words to quote (and the app quotes anyone). When false, ask nothing: the consent stays as it is, and no quote is kept. `quote_choices` stays the set an answer is checked against."
          },
          "quote_reader": {
            "type": "string",
            "nullable": true,
            "description": "Who reads a quote, when the app names one person (a team conversation's leader)."
          },
          "quote_consent": {
            "type": "string",
            "enum": [
              "none",
              "deidentified",
              "with_name"
            ]
          },
          "who_sees_now": {
            "type": "string",
            "nullable": true,
            "description": "Who reads the answers, when that changed since the person started (their manager became an administrator) — said again before Send."
          },
          "felt_captured": {
            "type": "integer",
            "nullable": true
          },
          "can_submit": {
            "type": "boolean"
          }
        }
      },
      "AiInterviewAnswer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "item_key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "exploration",
              "measure"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "proposed",
              "confirmed",
              "edited",
              "removed"
            ]
          },
          "label": {
            "type": "string"
          },
          "can_remove": {
            "type": "boolean"
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "An exploration's — what will be recorded."
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The themes it was counted under",
            "in plain words.": null
          },
          "quotes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Words that might be quoted",
            "with details removed.": null
          },
          "prompt": {
            "type": "string",
            "description": "A measure's question."
          },
          "scale": {
            "type": "string",
            "description": "A measure's: nps, rating, boolean, single, multiselect …"
          },
          "multiple": {
            "type": "boolean",
            "description": "A measure's answer is a list of options (`value` an array)."
          },
          "value": {
            "description": "A measure's answer.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "value_label": {
            "type": "string",
            "nullable": true
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "integer"
                    }
                  ]
                },
                "label": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "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"
          },
          "can_delete": {
            "type": "boolean",
            "description": "Whether DELETE /ai_notepad/notebooks/{id} will accept this caller (the creator, or an \"owner\" collaborator). This list also holds notebooks the caller can only edit, so hide Delete when false instead of offering a door that answers 403."
          },
          "can_file": {
            "type": "boolean",
            "description": "Whether a note may be filed into this notebook. False for a notebook shared view-only (listed only with `include=readable`) and for a token without `write:ai_notepad`."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NotepadMeetingRef": {
        "type": "object",
        "description": "Lightweight meeting reference for deep-linking.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Null when `can_open` is false — the caller holds an action item on a note nobody shared with them, and a note's title goes only to someone who can open it."
          },
          "can_open": {
            "type": "boolean",
            "description": "Whether the caller may open the note. When false, show the date and \"a note that isn't shared with you\", and do not link the row."
          },
          "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."
      },
      "NotepadSpeakerSuggestion": {
        "type": "object",
        "description": "One \"Who is Speaker A?\" suggestion.",
        "properties": {
          "speaker_id": {
            "type": "string",
            "description": "The roster id it is for (`them_a`, `them_b`, … or `them`).",
            "example": "them_a"
          },
          "label": {
            "type": "string",
            "description": "The roster's own name for that speaker (\"Speaker A\", \"Others on the call\")."
          },
          "name": {
            "type": "string",
            "description": "The invitee proposed.",
            "example": "Micki Chen"
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "The invitee's address, when the invite carries one."
          },
          "evidence": {
            "type": "string",
            "description": "The transcript's words, verbatim, that show who it is.",
            "example": "Thanks, Micki — can you walk us through the renewal terms?"
          }
        }
      },
      "NotepadSpeakerNaming": {
        "type": "object",
        "description": "What a client redraws after a speaker is named, a suggestion accepted or one dismissed.",
        "properties": {
          "speakers": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "speaker_suggestions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotepadSpeakerSuggestion"
            }
          },
          "speaker_names_changed": {
            "type": "boolean",
            "description": "See the meeting detail's `speaker_names_changed`."
          }
        }
      },
      "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."
              },
              "copy_text": {
                "type": "string",
                "description": "The whole note as the web's \"Copy notes\" puts it on the clipboard — My notes, summary, key insights and action items as Markdown, in page order. Empty string when there is nothing to copy, so a client can disable its Copy control. Present for every source. A recording's typed notes appear here under \"Notes\" even though `notes` and `notes_html` are null for a recording."
              },
              "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_delete": {
                "type": "boolean",
                "description": "Whether this caller may delete the note (DELETE /ai_notepad/meetings/{id}) and restore it from Recently Deleted — the creator, or an \"owner\" collaborator. A shared editor or viewer, or someone reading through a notebook, gets false; hide Delete instead of offering a door that answers 403."
              },
              "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."
              },
              "can_change_notebook": {
                "type": "boolean",
                "description": "Whether this caller may move the note to another notebook (PATCH /ai_notepad/meetings/{id}/notebook). Filing a note into a notebook hands that notebook's audience the note, so this is narrower than `can_edit`; gate the notebook picker on it."
              },
              "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
                }
              },
              "speaker_suggestions": {
                "type": "array",
                "description": "\"Who is Speaker A?\" After processing, one AI pass proposes an invitee for each far-side speaker nobody has named — only with words from the transcript that show it (they introduce themselves, or are addressed by name and answer), never from the invite list alone. These are the live ones: pending, for a speaker still unnamed, made from the current transcript. Offer them to an editor (`can_edit`) through POST /ai_notepad/meetings/{id}/speakers/{speaker_id}/suggestion/accept and …/dismiss.",
                "items": {
                  "$ref": "#/components/schemas/NotepadSpeakerSuggestion"
                }
              },
              "speaker_names_changed": {
                "type": "boolean",
                "description": "A speaker was named (or un-named) after the summary was written, so the summary and action items still carry the old label. Offer \"Update notes with these names\" — POST /ai_notepad/meetings/{id}/regenerate, which skips transcription when a transcript exists. Clears when that run starts."
              },
              "account_suggestion": {
                "type": "object",
                "nullable": true,
                "description": "The Mango CS customer the automatic linker matched this note to, waiting on its owner to confirm (finding #4182 — notes are suggested, never linked, because linking makes the note readable by every Mango CS user). Confirm with POST /ai_notepad/meetings/{id}/account and this `id` as `crm_account_id`; dismiss with POST /ai_notepad/meetings/{id}/account/suggestion/dismiss. Null unless the caller may confirm it (Mango CS open to them, the note unlinked and not in Recently Deleted, and the caller the note's creator or a direct owner, or a direct editor while “Editors can pass a note on” is on).",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "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": {
              "note_template": {
                "type": "object",
                "nullable": true,
                "description": "The note template that shaped the AI notes; null for Auto or a note never reshaped. Draw the badge beside Key insights from it and tick it in the reshape menu.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "workspace": {
                "type": "object",
                "nullable": true,
                "description": "The workspace this note is linked to (its action items become that workspace's to-dos), or null. `name` is null when the workspace has since been moved to Trash, or when it is private and the caller cannot open it. Change it with PATCH /ai_notepad/meetings/{id}/workspace.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "nullable": true
                  },
                  "available": {
                    "type": "boolean",
                    "description": "False once the workspace has been moved to Trash."
                  },
                  "openable": {
                    "type": "boolean",
                    "description": "True when the caller can open the workspace; link to it only then."
                  }
                }
              },
              "series": {
                "type": "object",
                "nullable": true,
                "description": "The recurring series this note belongs to (two notes by the same person on occurrences of one calendar event, or with the same calendar name within 60 days): the previous occurrence's note and this note's position. Null when there is no earlier note. The \"since last time\" content itself is an ordinary insights section (`since_last_time`, first in the artifacts' `sections`) — nothing to render specially; this is the link a client draws \"← previous\" from.",
                "properties": {
                  "previous_meeting": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "title": {
                        "type": "string"
                      },
                      "occurred_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      }
                    }
                  },
                  "count": {
                    "type": "integer",
                    "description": "This note's position in the series (1-based)."
                  }
                }
              },
              "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
          },
          "recording_deleted_after_restore": {
            "type": "boolean",
            "description": "Restore only. True when the note's recording came back with it but its audio-retention window ends before the next nightly cleanup, which then deletes it (the web's restore notice says the same). Recently Deleted itself never loses a recording to that cleanup."
          }
        }
      },
      "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",
                  "processing_unavailable"
                ]
              }
            }
          },
          "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"
              }
            }
          }
        }
      },
      "NotepadNoteTemplate": {
        "type": "object",
        "description": "One of the tenant's note templates, reduced to what a picker shows.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "system_template": {
            "type": "boolean",
            "description": "Seeded by MangoApps (true) or authored in this workspace (false)."
          },
          "favorite": {
            "type": "boolean",
            "description": "Whether the caller has starred this template. Favourites are listed first."
          },
          "usage_count": {
            "type": "integer"
          },
          "sections": {
            "type": "array",
            "description": "The headings the AI notes take under this template, in order.",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "description": "`text` or `checklist`."
                }
              }
            }
          }
        }
      },
      "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",
              "cancelled"
            ]
          },
          "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"
          },
          "linked_task_state": {
            "type": "string",
            "nullable": true,
            "enum": [
              "still_open",
              "still_done",
              null
            ],
            "description": "Set when the item was converted into a Task and that Task disagrees with the item: `still_open` — the item is ticked or cancelled but its Task is still open in Tasks (the Task needs a completion photo or notes, or the person who ticked may not complete it); `still_done` — the item was un-ticked but its Task is still completed. `null` when there is no linked Task or the two agree. Show it beside the tick."
          }
        }
      },
      "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
          },
          "dismissed": {
            "type": "boolean",
            "description": "True when the person already answered the pop-up for this occurrence on the web, the phone or this client (`POST /meetings/upcoming/dismiss`). The row stays in the feed — it is still a meeting to start a capture from — so skip the pop-up, not the row."
          },
          "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, as it\nreads today — \"Deleted user\" for a reviewer since erased or gone. 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"
              }
            }
          }
        }
      },
      "AnalyticsRefusal": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string",
            "example": "forbidden"
          }
        }
      },
      "SentimentAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "alert_type": {
            "type": "string",
            "enum": [
              "spike",
              "confusion",
              "policy_resistance",
              "culture_concern"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "description": {
            "type": "string"
          },
          "detected_at": {
            "type": "string",
            "format": "date-time"
          },
          "segment_id": {
            "type": "integer",
            "nullable": true
          },
          "segment_name": {
            "type": "string",
            "nullable": true,
            "description": "Always null in the dismiss response."
          },
          "contributing_post_count": {
            "type": "integer"
          },
          "metadata": {
            "type": "object"
          },
          "status": {
            "type": "string",
            "example": "dismissed"
          },
          "dismissed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "dismissed_reason": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AppointmentSchedulerRefusal": {
        "type": "object",
        "description": "This controller's own refusal envelope.",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "slot_taken",
              "not_cancellable",
              "not_found",
              "invalid"
            ],
            "description": "Present on a 422 from cancel or reschedule only."
          }
        }
      },
      "AppointmentSchedulerPaginationMeta": {
        "type": "object",
        "required": [
          "total",
          "page",
          "per_page",
          "total_pages"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Rows across all pages."
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          }
        }
      },
      "AppointmentSchedulerSlot": {
        "type": "object",
        "required": [
          "starts_at",
          "ends_at"
        ],
        "properties": {
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "description": "UTC, ISO 8601 (`...Z`)."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "description": "UTC, ISO 8601 (`...Z`)."
          }
        }
      },
      "AppointmentSchedulerBookingPage": {
        "type": "object",
        "required": [
          "id",
          "title",
          "slug",
          "public_path",
          "active",
          "bookable",
          "public_booking_disabled",
          "unbookable_reason",
          "visibility",
          "timezone",
          "owner",
          "relationship",
          "meeting_types"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "public_path": {
            "type": "string",
            "description": "`/meet/<slug>` on the business's own host.",
            "example": "/meet/sales-demo"
          },
          "active": {
            "type": "boolean"
          },
          "bookable": {
            "type": "boolean",
            "description": "The page takes bookings right now: active, not expired, with an active meeting type and a host left to serve it (or a live routing question) — and the business has public booking switched on."
          },
          "public_booking_disabled": {
            "type": "boolean",
            "description": "The business has **Allow booking via public /meet pages** switched off, so every /meet link answers \"unavailable\"."
          },
          "unbookable_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "inactive",
              "expired",
              "no_active_type",
              "no_host",
              null
            ],
            "description": "Why the page itself can't take bookings; null when it can (a page that books only through its routing question reports null)."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "link_only",
              "public",
              "org_only"
            ],
            "description": "`link_only` — \"Link only — not listed anywhere\"; anyone with the link can book. `public` — \"Listed in the org directory\"; anyone with the link can book, and a listed SOLO page is what the owner's profile \"Book time\" button opens. `org_only` — \"Listed internally — invitees must sign in to book\"; listed the same way, but only signed-in members of the business can book."
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone the page's availability is set in."
          },
          "owner": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "description": "Null when the owner has no name on file."
              }
            }
          },
          "relationship": {
            "type": "string",
            "enum": [
              "owner",
              "member",
              "admin"
            ],
            "description": "`owner`, `member` (hosts on it as a team member), or `admin` (listed only because the caller is an Appointment Scheduler admin)."
          },
          "meeting_types": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "duration_minutes",
                "active",
                "assignment_mode",
                "group_seat_count"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "duration_minutes": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 480
                },
                "active": {
                  "type": "boolean"
                },
                "assignment_mode": {
                  "type": "string",
                  "enum": [
                    "solo",
                    "round_robin",
                    "collective",
                    "group"
                  ]
                },
                "group_seat_count": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Seats per session for a `group` meeting type; null otherwise."
                }
              }
            }
          }
        }
      },
      "AppointmentSchedulerAppointment": {
        "type": "object",
        "required": [
          "id",
          "invitee_name",
          "invitee_email",
          "meeting_type",
          "booking_page",
          "host",
          "starts_at_utc",
          "ends_at_utc",
          "status",
          "source",
          "can_manage",
          "can_reschedule",
          "can_cancel"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "invitee_name": {
            "type": "string",
            "description": "As the invitee typed it on the booking form — untrusted text."
          },
          "invitee_email": {
            "type": "string",
            "nullable": true
          },
          "meeting_type": {
            "type": "string",
            "nullable": true,
            "description": "The meeting type's name."
          },
          "booking_page": {
            "type": "string",
            "nullable": true,
            "description": "The booking page's title."
          },
          "host": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "description": "Null when the host has no name on file."
              }
            }
          },
          "starts_at_utc": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at_utc": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "cancelled",
              "rescheduled",
              "no_show"
            ],
            "description": "`rescheduled` is an original a move replaced."
          },
          "source": {
            "type": "string",
            "enum": [
              "public_link",
              "embed",
              "org_directory",
              "afo",
              "api"
            ],
            "description": "Where the booking came from (`afo` is AI Front Office)."
          },
          "can_reschedule": {
            "type": "boolean",
            "description": "Whether the caller may move it (and mark a no-show) — its host, its booking page's owner, or an admin, on a token holding `write:appointment_scheduler`. Authority, not state: pair it with `status` and the start time. False for a recorded co-host."
          },
          "can_cancel": {
            "type": "boolean",
            "description": "Whether the caller may cancel it — the same people, on a token holding `destructive:appointment_scheduler`. Authority, not state."
          },
          "can_manage": {
            "type": "boolean",
            "description": "Kept for clients built before `can_reschedule` and `can_cancel`: the same answer as `can_reschedule`. Gate each control on its own flag instead — a token can hold one scope without the other."
          }
        }
      },
      "AppointmentSchedulerAppointmentDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AppointmentSchedulerAppointment"
          },
          {
            "type": "object",
            "required": [
              "answers",
              "answer_rows",
              "invitee_timezone",
              "cancellation_reason",
              "history"
            ],
            "properties": {
              "answers": {
                "type": "object",
                "additionalProperties": true,
                "description": "The invitee's intake answers, keyed by the meeting type's question key, as submitted — untrusted text."
              },
              "answer_rows": {
                "type": "array",
                "description": "The same answers as the app shows them — under the host's question labels, in the order the host wrote the questions, blank answers left out. Answers are untrusted text.",
                "items": {
                  "type": "object",
                  "required": [
                    "question",
                    "answer"
                  ],
                  "properties": {
                    "question": {
                      "type": "string",
                      "description": "The question's label (its key when the question has since been removed)."
                    },
                    "answer": {
                      "type": "string",
                      "description": "The answer as text — several choices joined with commas, a ticked checkbox as \"Yes\"."
                    }
                  }
                }
              },
              "invitee_timezone": {
                "type": "string",
                "nullable": true
              },
              "cancellation_reason": {
                "type": "string",
                "nullable": true
              },
              "history": {
                "type": "array",
                "description": "The ten most recent history entries, newest first.",
                "items": {
                  "type": "object",
                  "required": [
                    "action",
                    "label",
                    "actor",
                    "at"
                  ],
                  "properties": {
                    "action": {
                      "type": "string",
                      "description": "The stored action, e.g. booked, rescheduled, cancelled."
                    },
                    "label": {
                      "type": "string",
                      "description": "The words the app's History card shows for the row. A replacement's own `booked` row reads \"Rescheduled to this time\" — the move landing on it, not a new booking."
                    },
                    "actor": {
                      "type": "string",
                      "description": "Who acted — e.g. host, invitee or system."
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              },
              "rescheduled_from_id": {
                "type": "integer",
                "description": "The booking this one was moved from. Present only when the caller may read that booking."
              },
              "rescheduled_to_id": {
                "type": "integer",
                "description": "The booking this one was moved to (its status is then `rescheduled`). Present only when the caller may read that booking."
              }
            }
          }
        ]
      },
      "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 — returned (post-decision) by the decision\nendpoints, and inside GET /comms_hub/approval_requests/{id}. (A\nsource's index `pending_approvals` item carries only its id, as\n`approval_id`.) `can_act`, `can_reject` and `can_withdraw` are\nwhat the caller may do now, as the decision doors will answer: the\nperson's rule and the token's scope together.\n",
        "required": [
          "request_id",
          "status",
          "requested_at",
          "current_step",
          "can_act",
          "can_reject",
          "can_withdraw"
        ],
        "properties": {
          "request_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "withdrawn",
              "overridden"
            ]
          },
          "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",
            "description": "Whether the caller can approve the current step now — never their own request (another approver decides it) unless they are a company admin, and never on a token without `write:broadcasts`."
          },
          "can_reject": {
            "type": "boolean",
            "description": "`can_act`, and the token carries rejecting's delete-level scope (`destructive:broadcasts`; an alert's, `destructive:safety_hub`)."
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "Whether `POST /approvals/{id}/withdraw` takes it back for the caller now: it is pending, the caller asked for it or is a company admin (while Approvals is off, only the person who asked), and the token carries `write:broadcasts` and the delete-level scope."
          },
          "content_version": {
            "type": "integer",
            "nullable": true,
            "description": "The item's version as Unix seconds — pass it back as `seen` on\napprove/reject to refuse a decision on a version you didn't read.\n"
          }
        }
      },
      "ApprovalDecision": {
        "type": "object",
        "description": "Response from POST /approvals/{id}/approve, /reject and /withdraw.",
        "required": [
          "source_type",
          "source_id",
          "approval"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "Approve and reject only — what the decision did, in the web queue's\nwords: \"Approved. The communication is released to send.\", a step\nhand-off (\"Approved. Passed to step 2 of 3.\"), \"Approved, but it\nwon't go out: its author has left the company.\", or whether the\nauthor was told about a rejection.\n"
          },
          "source_type": {
            "type": "string",
            "description": "The decided item's source type — a post, a broadcast, an alert or a newsletter issue.",
            "enum": [
              "NewsFeed::Feed",
              "Broadcast",
              "Alert",
              "CommsHub::Issue"
            ]
          },
          "source_id": {
            "type": "integer",
            "description": "The source record's id (the post, broadcast, alert or newsletter issue)."
          },
          "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"
          }
        }
      },
      "AskAiAttachmentResponse": {
        "type": "object",
        "properties": {
          "attachment": {
            "$ref": "#/components/schemas/AskAiAttachment"
          }
        }
      },
      "AskAiAttachment": {
        "type": "object",
        "properties": {
          "signed_id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "status_url": {
            "type": "string",
            "description": "Present for videos; same-origin relative URL"
          },
          "processing": {
            "type": "object",
            "description": "Present for videos only. Extracted content is private and excluded.",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "queued",
                  "processing",
                  "ready",
                  "failed",
                  "cancelled",
                  "expired"
                ]
              },
              "message": {
                "type": "string"
              },
              "duration": {
                "type": "number"
              },
              "has_audio": {
                "type": "boolean"
              },
              "frames_done": {
                "type": "integer",
                "minimum": 0,
                "maximum": 6
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "Thread": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The thread id (session id)"
          },
          "type": {
            "type": "string",
            "example": "general"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ]
          },
          "message_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "object"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "maxLength": 100,
            "description": "The saved name or first question asked; null for an unnamed empty thread"
          },
          "pinned": {
            "type": "boolean",
            "description": "Whether the caller has pinned this private thread across clients"
          },
          "active": {
            "type": "boolean",
            "description": "The thread the next message lands in"
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the thread was last talked in — its newest visible message, or updated_at for a thread with none. Sent by the conversations list, which is ordered by it (pinned first)."
          }
        }
      },
      "ThreadResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "conversation": {
            "$ref": "#/components/schemas/Thread"
          }
        }
      },
      "MessageResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "context_app": {
            "type": "object",
            "nullable": true,
            "description": "Present only when the request sent `app_slug`. The app the turn is\nbound to, or null when the slug was not honoured (unknown, not open\nto this person, or no agent for it). Show an \"Asking about …\"\nindicator only when this is non-null.\n",
            "properties": {
              "slug": {
                "type": "string",
                "example": "tasks"
              },
              "name": {
                "type": "string",
                "example": "Tasks"
              }
            }
          },
          "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.",
            "description": "The message text. An answer grounded in company knowledge lists its\nsources only in `metadata.provenance.sources` (or, for rows that nest\nit, `metadata.metadata.provenance.sources`) — its text carries no\n\"Sources\" list, so render that list.\n"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "ai_generated": {
            "type": "boolean",
            "example": true,
            "description": "Machine-readable AI-output mark (EU AI Act Art. 50(2)): true on a\nstored assistant answer, false on the person's own words. The web\ntranscript's `data-ai-generated` attribute and the live `complete`\nframe say the same.\n"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Allowlisted message metadata (see the Ask AI guide, \"Message\nmetadata\"). The row's own keys may sit one level down under\n`metadata`; read each key at its current path.\n",
            "properties": {
              "provenance": {
                "$ref": "#/components/schemas/AnswerProvenance"
              },
              "turn_id": {
                "type": "string",
                "description": "Exact answer identity for feedback and correction requests."
              },
              "feedback_allowed": {
                "type": "boolean",
                "description": "False when the caller cannot submit feedback, including read-only tokens and tool-confirmation outcome rows; hide rating controls when false."
              },
              "correctable": {
                "type": "boolean",
                "description": "False when this answer cannot accept a knowledge correction."
              },
              "answer_feedback": {
                "type": "object",
                "description": "The caller's saved feedback. Staff review fields are never included.",
                "properties": {
                  "rating": {
                    "type": "string",
                    "enum": [
                      "up",
                      "down"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "incorrect",
                      "missing",
                      "off_topic",
                      "technical"
                    ]
                  },
                  "comment": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              },
              "escalated_label": {
                "type": "string",
                "description": "Present on an answer already escalated with \"Get human help\":\nwhere it went, in the web's own words (\"Sent to support (#123)\non Oct 12\"), in the tenant's language. An escalated answer\ncarries no `escalation_offer` — do not offer the button again.\n",
                "example": "Sent to support (#123) on Oct 12"
              },
              "escalated_ticket_url": {
                "type": "string",
                "format": "uri",
                "description": "The escalation's Service Desk ticket page (absolute URL), for\nlinking `escalated_label` as the web and phone do. Present only\nwhen the answer went to a ticket and Service Desk opens for\nthis person.\n",
                "example": "https://acme.workforce.mangoapps.com/apps/service-desk/123"
              }
            }
          }
        }
      },
      "AnswerProvenance": {
        "type": "object",
        "description": "Trust badges and the panels under an answer — the same shape on a\nreloaded message and on the live `complete` frame. Lists are present\nonly when non-empty.\n",
        "additionalProperties": true,
        "properties": {
          "kind": {
            "type": "string",
            "description": "`computed`, `live_data`, `retrieved`, `inference`, …"
          },
          "citation_count": {
            "type": "integer",
            "description": "Documents the answer was grounded in."
          },
          "verified_count": {
            "type": "integer"
          },
          "verified_entry_ids": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "integer"
            }
          },
          "sources": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/AnswerSource"
            }
          },
          "linked_sources": {
            "type": "array",
            "maxItems": 10,
            "description": "The records an agent's answer LINKS in its own text — sources a\nknowledge tool retrieved and the answer then linked (Knowledge Layer\nA3). They are already visible inline, so do NOT draw them as a\nsecond list; they are the answer's sources all the same, with the\nversion it used and, on a reloaded answer, `changed_since` — show\n\"A source changed since this answer\" from either list. A record the\ntool retrieved but the answer never linked is never here.\n",
            "items": {
              "$ref": "#/components/schemas/AnswerSource"
            }
          },
          "answered_for": {
            "type": "object",
            "description": "\"Answered for Ohio · hourly\" (Knowledge Layer A2) — the asker's own\nprofile facts the answer was scoped by, for a client's \"Not you?\".\nPresent only when the answer was scoped to the asker.\n",
            "required": [
              "label",
              "facts"
            ],
            "properties": {
              "label": {
                "type": "string",
                "description": "The facts in short, joined with \" · \" (a client prefixes \"Answered for\").",
                "example": "Ohio · hourly"
              },
              "facts": {
                "type": "array",
                "maxItems": 6,
                "description": "The profile facts behind `label` (\"From your profile\").",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string",
                      "example": "Work location"
                    },
                    "value": {
                      "type": "string",
                      "example": "Store 118, Columbus"
                    }
                  }
                }
              },
              "profile_path": {
                "type": "string",
                "description": "A relative same-origin path to the asker's profile overview, where a wrong fact is corrected."
              }
            }
          }
        }
      },
      "AnswerSource": {
        "type": "object",
        "description": "A record the answer was drawn from. `title` is always present. The\nfields after `excerpt` are present on an answer grounded in company\nknowledge and absent otherwise. Every source has already passed the\nasker's own access check. `title` and `excerpt` are plain text —\nescape them for your medium.\n",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 200,
            "example": "Paid Time Off Policy"
          },
          "url": {
            "type": "string",
            "description": "A relative same-origin path — open it against the workspace origin. Absent when the record has no page in the workspace; show the title as plain text, or link `external_url` when present.",
            "example": "/apps/policy-hub/policies/42"
          },
          "external_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 400,
            "description": "An absolute `http`/`https` link outside the workspace — a\nknowledge-base article's own source (an external page, a hosted\nvideo). Never present together with `url`, never another scheme.\nOpen it outside the app (system browser or an in-app browser tab),\nnever as a workspace path. Absent for every other source.\n",
            "example": "https://docs.example.com/benefits/remote-work"
          },
          "excerpt": {
            "type": "string",
            "maxLength": 200
          },
          "marker": {
            "type": "string",
            "description": "The inline citation this source answers to.",
            "example": "[2]"
          },
          "source_type": {
            "type": "string",
            "description": "`hr_policy`, `sop`, `wiki`, `site_page`, `drive_file`, `knowledge_base_entry`, `contract`, `recording`, `ai_note`, …",
            "example": "hr_policy"
          },
          "record_id": {
            "type": "integer",
            "example": 42
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the document last changed."
          },
          "review_overdue": {
            "type": "boolean",
            "description": "The document's scheduled review is overdue — show it."
          },
          "owner": {
            "type": "string",
            "maxLength": 200,
            "description": "Who answers for the document, named as its own page names it: the\nowner, else the owning team, else the author standing in (\"Dana Lee\n(author)\"). Absent when nobody still here answers for it.\n",
            "example": "Dana Lee"
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date the document's governance clock reads — see `reviewed_kind`."
          },
          "reviewed_kind": {
            "type": "string",
            "enum": [
              "reviewed",
              "updated"
            ],
            "description": "`reviewed` when `reviewed_at` is a real review (someone confirmed it is still right); `updated` when it is only the last publish or edit."
          },
          "verified": {
            "type": "boolean",
            "description": "Present (true) for an answer your organization verified. Absent otherwise."
          },
          "applies_to_you": {
            "type": "boolean",
            "description": "For a policy or SOP narrowed to an audience or a jurisdiction, whether it applies to the person asking. Absent when it applies to everyone."
          },
          "applies_label": {
            "type": "string",
            "description": "The chip a source that is NOT for the asker carries — its own\nApplies to, worded (Knowledge Layer A2). Present only when\n`applies_to_you` is false.\n",
            "example": "For California employees"
          },
          "language": {
            "type": "string",
            "description": "The language the source is written in (a language code).",
            "example": "en"
          },
          "language_label": {
            "type": "string",
            "description": "\"In English\" — present only when the source's language is not the asker's.",
            "example": "In English"
          },
          "trust_line": {
            "type": "array",
            "description": "The one trust line exactly as the web draws it (Knowledge Layer \"one\ntrust line\") — `language_label` when set, then ONE trust word\n(\"Review overdue\", \"Verified\", \"Reviewed Aug 2026\" or \"Updated Aug\n2026\"), then the owner (\"Owned by Dana Lee\"). `warning: true` marks\nthe word to draw as a warning (review overdue). Render it as given,\njoined with \" · \"; never re-derive the words. Absent when nothing is\nknown.\n",
            "items": {
              "type": "object",
              "required": [
                "text",
                "warning"
              ],
              "properties": {
                "text": {
                  "type": "string",
                  "example": "Reviewed Aug 2026"
                },
                "warning": {
                  "type": "boolean"
                }
              }
            },
            "example": [
              {
                "text": "Verified",
                "warning": false
              },
              {
                "text": "Owned by Dana Lee",
                "warning": false
              }
            ]
          },
          "version_id": {
            "type": "integer",
            "description": "The published version of the source this answer used — its policy,\nSOP, wiki page or Sites page version (the answer record, Knowledge\nLayer A3). Absent for a source that keeps no versions (a\nknowledge-base entry) or had no published version at the time.\n",
            "example": 318
          },
          "version_number": {
            "type": "integer",
            "description": "That version's number, as the source's own history numbers it (\"v4\").",
            "example": 4
          },
          "changed_since": {
            "type": "boolean",
            "description": "Present (true) on a reloaded answer — the conversation messages\nendpoint — when the source has published a MATERIAL change since\nthis answer used it (its app's own change types decide: a policy's\nmajor or legal change, a Sites republish with a \"What changed?\"\nnote; a typo fix never counts). Show \"A source changed since this\nanswer\" and offer to ask again. Absent otherwise.\n"
          }
        }
      },
      "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"
              }
            }
          }
        }
      },
      "AskAiSavedAnswer": {
        "type": "object",
        "required": [
          "id",
          "saved_at",
          "available"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Platform bookmark id used for removal and pagination."
          },
          "saved_at": {
            "type": "string",
            "format": "date-time"
          },
          "available": {
            "type": "boolean",
            "description": "Whether this account may still read the answer. Other fields are omitted when false."
          },
          "conversation_id": {
            "type": "string",
            "description": "Private conversation session id; present only when available."
          },
          "turn_id": {
            "type": "string",
            "description": "Durable answer turn id; open the original conversation at this turn."
          },
          "question": {
            "type": "string",
            "maxLength": 500,
            "description": "Original visible user question, or nearest preceding visible question for legacy transcripts; empty when none survives."
          },
          "excerpt": {
            "type": "string",
            "maxLength": 500,
            "description": "Plain-text preview of the current answer with Markdown syntax removed."
          }
        }
      },
      "AskAiSettings": {
        "type": "object",
        "description": "Feature settings for the mobile AI client, including current conversation for session restoration",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "assistant_name": {
            "type": "string",
            "description": "The name the company gave its assistant, for the Ask AI screen and buttons.\nA name the company chose comes back as typed; otherwise the default name,\ntranslated into the reader's language.\n",
            "example": "Morris"
          },
          "assistant_name_custom": {
            "type": "boolean",
            "description": "True when the company renamed its assistant. When false, `assistant_name`\nis the platform default and a client may use its own localized \"Ask AI\" label.\n",
            "example": true
          },
          "composer_assist": {
            "type": "object",
            "description": "Supported wire modes and context formats; flags separately gate availability.",
            "properties": {
              "modes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "formats": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "chat_body"
                  ]
                }
              },
              "context_scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "room",
                    "thread"
                  ]
                }
              }
            }
          },
          "smart_replies": {
            "type": "object",
            "properties": {
              "context_scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "room",
                    "thread"
                  ]
                }
              }
            }
          },
          "settings": {
            "type": "object",
            "properties": {
              "saved_answers_enabled": {
                "type": "boolean",
                "description": "Whether the client may show the saved-answer list."
              },
              "save_answers_enabled": {
                "type": "boolean",
                "description": "Whether the caller has write:chat scope to save or remove answers."
              },
              "create_support_ticket_enabled": {
                "type": "boolean",
                "description": "Whether Ask AI may file Service Desk tickets for this user (Service Desk reachable and its chat integration on)",
                "example": true
              },
              "escalation_enabled": {
                "type": "boolean",
                "description": "Whether to offer \"Get human help\" under an answer whose metadata has\n`escalation_offer: true` and a `turn_id`. The same gate\n`POST /ask_ai/escalate` enforces.\n",
                "example": true
              },
              "show_create_ticket_button": {
                "type": "boolean",
                "description": "Whether to show a standing \"Create Support Ticket\" button. Off unless the\nService Desk admin turned it on — the web chat applies the same rule.\n",
                "example": false
              },
              "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": "object",
            "description": "`code` is `payment_required` on the 402 this schema documents;\n`message` is the refusal sentence for display.\n",
            "properties": {
              "code": {
                "type": "string",
                "example": "payment_required"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "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)"
          }
        }
      },
      "AskAiSuccess": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "AskAiMentionableAgent": {
        "type": "object",
        "properties": {
          "alias": {
            "type": "string",
            "description": "What a message starts with to reach this agent (`@<alias> …`).",
            "example": "scheduling"
          },
          "name": {
            "type": "string",
            "example": "Scheduling Assistant"
          },
          "description": {
            "type": "string",
            "description": "The agent's tagline, or its description cut to 60 characters; may be empty."
          },
          "icon": {
            "type": "string",
            "description": "A Font Awesome class.",
            "example": "fa-robot"
          }
        }
      },
      "AskAiPinnedAgent": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "The agent's name for the chip.",
            "example": "Scheduling Assistant"
          },
          "domain": {
            "type": "string",
            "description": "The agent's alias.",
            "example": "scheduling"
          },
          "agent": {
            "type": "string",
            "description": "The agent's internal class name."
          }
        }
      },
      "AskAiPrompt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Empty when the prompt has none."
          },
          "agent_slug": {
            "type": "string",
            "nullable": true,
            "description": "The agent the prompt is meant for; null or empty for any."
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "prompt_content": {
            "type": "string",
            "description": "The prompt text."
          },
          "variables": {
            "type": "array",
            "description": "The prompt's fill-in variables. Every entry has a non-blank `name`; other keys are as the author saved them.",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "system",
              "tenant"
            ],
            "description": "`system` for the shared gallery, `tenant` for this business's own prompts."
          },
          "featured": {
            "type": "boolean"
          }
        }
      },
      "AskAiPromptsResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "prompts": {
            "type": "array",
            "description": "System gallery prompts first, then this business's prompts.",
            "items": {
              "$ref": "#/components/schemas/AskAiPrompt"
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the gallery or the business's own prompts have more than `limit`."
          },
          "limit": {
            "type": "integer",
            "description": "The most prompts returned from each of the two lists.",
            "example": 200
          }
        }
      },
      "AskAiUsedPrompt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "agent_slug": {
            "type": "string",
            "nullable": true,
            "description": "The agent to send the draft to; null or empty for normal routing."
          },
          "prompt_content": {
            "type": "string",
            "description": "The text to put in the composer."
          }
        }
      },
      "AskAiMemoryFact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "content": {
            "type": "string",
            "example": "Prefers morning shifts"
          },
          "fact_type": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "remembered_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the fact was learned."
          },
          "last_referenced_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an answer last used it."
          },
          "sample": {
            "type": "boolean",
            "description": "True for a fact loaded as sample data, not learned from the person's chats."
          }
        }
      },
      "AskAiMemoryResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "memory_enabled": {
            "type": "boolean",
            "description": "Whether personal memory is turned on for this business. Facts already\nremembered are listed either way.\n"
          },
          "facts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AskAiMemoryFact"
            }
          },
          "pagination": {
            "type": "object",
            "properties": {
              "total_count": {
                "type": "integer"
              },
              "current_page": {
                "type": "integer"
              },
              "per_page": {
                "type": "integer",
                "example": 50
              },
              "total_pages": {
                "type": "integer",
                "description": "At least 1, even with no facts."
              }
            }
          }
        }
      },
      "AskAiConfirmationRow": {
        "type": "object",
        "description": "One field the action will change, as the confirmation card prints it.",
        "properties": {
          "label": {
            "type": "string",
            "description": "The field's name, in the person's display language."
          },
          "from": {
            "type": "string",
            "nullable": true,
            "description": "The value now; null when the action creates the record."
          },
          "to": {
            "type": "string",
            "description": "The value after the action, as display text (\"Not set\" when empty)."
          }
        }
      },
      "AskAiPendingConfirmation": {
        "type": "object",
        "description": "An action waiting for approval. Approve or decline it with\n`POST /ask_ai/confirm_tool` and this `id`.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The `confirmation_id` to send."
          },
          "tool": {
            "type": "string",
            "nullable": true,
            "description": "The tool the action runs; null for a mix of tools or a playbook step."
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "maxLength": 200,
            "description": "The question as one line of plain text — escape it for your medium."
          },
          "destructive": {
            "type": "boolean",
            "description": "Draw Confirm as a destructive button and ask for a second click."
          },
          "irreversible": {
            "type": "boolean",
            "description": "Say \"can't be undone\". True only when `destructive` is true."
          },
          "fields": {
            "type": "array",
            "description": "What a single-record action changes. Empty when the card shows no changes.",
            "items": {
              "$ref": "#/components/schemas/AskAiConfirmationRow"
            }
          },
          "records": {
            "type": "array",
            "description": "The records a bulk action changes. Empty when it is not a bulk action.",
            "items": {
              "type": "object",
              "properties": {
                "summary": {
                  "type": "string"
                },
                "changes": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AskAiConfirmationRow"
                  }
                },
                "invalid": {
                  "type": "boolean",
                  "description": "True for a record the action cannot apply to."
                }
              }
            }
          }
        }
      },
      "AskAiConfirmToolResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Whether the action did what was asked. Can be false on a 200 — the\nconfirmation was handled but the action reported a failure, explained\nin `text` (and `error`).\n"
          },
          "text": {
            "type": "string",
            "description": "What happened, to show in the conversation (or \"Action cancelled — no changes were made.\" on a decline)."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "The action's own error, when it reported one."
          },
          "ui_response": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "The Ask AI component envelope (`{ components: [{ type, props }] }`)\nfor the next approval card or a resumed playbook's output. Its\ncomponents vary by agent.\n"
          },
          "follow_ups": {
            "type": "object",
            "additionalProperties": true,
            "description": "A button row component to show under the result (for example \"Open import review\"), when there is one."
          },
          "requires_confirmation": {
            "type": "boolean",
            "description": "Present (true) when the approved step stopped at another approval."
          },
          "confirmation_id": {
            "type": "string",
            "description": "The next approval's id, beside `requires_confirmation`."
          },
          "confirmation": {
            "$ref": "#/components/schemas/AskAiPendingConfirmation"
          }
        }
      },
      "AskAiEscalateResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "ticket_id": {
            "type": "integer",
            "nullable": true,
            "description": "The Service Desk ticket; null when the question went to the app's admins."
          },
          "routed_to": {
            "type": "string",
            "enum": [
              "service_desk",
              "app_admins"
            ]
          },
          "message": {
            "type": "string",
            "description": "The sentence to show, in the person's display language."
          }
        }
      },
      "AskAiCableTokenResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "cable_token": {
            "type": "string",
            "description": "Pass as the `cable_token` query parameter when opening the WebSocket."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the credential lasts.",
            "example": 3600
          },
          "websocket": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "description": "The ActionCable URL for this host.",
                "example": "wss://mycompany.workforce.mangoapps.com/cable"
              },
              "channel": {
                "type": "string",
                "example": "AiResponseChannel"
              }
            }
          }
        }
      },
      "AssetProFailure": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "AssetProAssetDetail": {
        "type": "object",
        "required": [
          "id",
          "name",
          "asset_number",
          "serial_number",
          "barcode",
          "model_number",
          "status",
          "status_label",
          "condition",
          "category",
          "category_id",
          "purchase_date",
          "warranty_end_date",
          "assigned_to",
          "location",
          "open_tickets_count",
          "last_service_ticket_at",
          "asset_kind",
          "custom_fields",
          "missing_info"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "asset_number": {
            "type": "string",
            "description": "The business's own asset number (unique within the business)."
          },
          "serial_number": {
            "type": "string",
            "nullable": true
          },
          "barcode": {
            "type": "string",
            "nullable": true
          },
          "model_number": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "assigned",
              "pending_assignment",
              "maintenance",
              "retired",
              "lost",
              "stolen"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "The status as Asset Pro's screens name it — \"Pending Approval\" for pending_assignment, otherwise the status in words (\"Available\").",
            "example": "Pending Approval"
          },
          "condition": {
            "type": "string",
            "nullable": true,
            "description": "One of excellent, good, fair, poor, damaged or unknown; null or an empty string when unset.",
            "example": "good"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "The asset category's name."
          },
          "category_id": {
            "type": "integer"
          },
          "purchase_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "warranty_end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "assigned_to": {
            "type": "object",
            "nullable": true,
            "description": "Who holds the asset; null when it is not assigned.",
            "required": [
              "id",
              "name",
              "email"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "description": "The holder's full name; an empty string when no name is on file."
              },
              "email": {
                "type": "string",
                "nullable": true,
                "description": "Shown only to the holder, or where the People directory shows emails to the caller; null otherwise."
              }
            }
          },
          "location": {
            "type": "object",
            "nullable": true,
            "description": "Where the asset is kept; null when no location is set.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "open_tickets_count": {
            "type": "integer",
            "description": "Open Service Desk tickets on the asset, as last counted when one of its tickets changed; 0 when none."
          },
          "last_service_ticket_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the most recent Service Desk ticket about the asset was submitted; null when it has none."
          },
          "asset_kind": {
            "type": "string",
            "enum": [
              "hardware",
              "software"
            ]
          },
          "manufacturer": {
            "type": "string",
            "nullable": true,
            "description": "Left out when the business has switched off the Asset Details section."
          },
          "department": {
            "type": "string",
            "nullable": true,
            "description": "The name of the department the asset belongs to. Left out when the business has switched off the Asset Details section."
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Left out when the business has switched off the Asset Details section."
          },
          "custom_fields": {
            "type": "array",
            "description": "The extra fields an admin added to the asset form (Asset Pro › Settings › Asset Form) that apply to this asset, in the admin's order. Every applicable field is listed, with a null value when it is empty, so every asset of a kind has the same shape.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label",
                "type",
                "value"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "description": "The field's fixed identifier. It never changes, even when an admin renames the field.",
                  "example": "imei"
                },
                "label": {
                  "type": "string",
                  "description": "The field's name as people see it; may change.",
                  "example": "IMEI"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "text",
                    "long_text",
                    "number",
                    "date",
                    "dropdown",
                    "yes_no"
                  ]
                },
                "value": {
                  "description": "A number for number fields, an ISO 8601 date string (YYYY-MM-DD) for date fields, true or false for yes_no fields, and a string otherwise; null when empty. A value device sync stored that is not valid for the field's type comes back as the string stored.",
                  "oneOf": [
                    {
                      "type": "string",
                      "nullable": true
                    },
                    {
                      "type": "number",
                      "nullable": true
                    },
                    {
                      "type": "boolean",
                      "nullable": true
                    }
                  ]
                }
              }
            }
          },
          "missing_info": {
            "type": "array",
            "description": "Keys of the required fields that are still empty on this asset: standard fields made required in Settings › Asset Form (for example `barcode`, `department`) first, then required extra fields by their key. A to-do for the people who manage assets (the asset page's \"Missing info\"): empty for a caller who can only view or hold the asset.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Connection": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "oauth",
              "token",
              "office_addin"
            ],
            "description": "oauth — an MCP client the caller signed in to; token — a personal API token; office_addin — an Office add-in sign-in (its id is the sign-in's refresh token)."
          },
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Claude"
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "example": "Signed in from Word",
            "description": "A line under the name where the kind has one — for an Office add-in sign-in, the Office app it signed in from. Null otherwise."
          },
          "client_uri": {
            "type": "string",
            "nullable": true,
            "example": "https://claude.ai"
          },
          "connected_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The latest assistant tool call made through this connection."
          },
          "is_current": {
            "type": "boolean",
            "description": "True for the token this request authenticated with."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "example": "read:my_data"
                },
                "description": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "BookingsPaginationMeta": {
        "type": "object",
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page",
          "has_next_page",
          "has_prev_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"
          }
        }
      },
      "BookingsSpaceRef": {
        "type": "object",
        "description": "The booked space, as a list row names it.",
        "required": [
          "id",
          "name",
          "space_type"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "space_type": {
            "type": "string",
            "enum": [
              "desk",
              "meeting_room",
              "parking_spot",
              "locker",
              "phone_booth",
              "av_equipment",
              "company_vehicle",
              "gym_equipment"
            ]
          }
        }
      },
      "BookingsHolder": {
        "type": "object",
        "description": "The person the booking is held by.",
        "required": [
          "id",
          "full_name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "full_name": {
            "type": "string",
            "description": "The holder's display name; may be empty."
          }
        }
      },
      "BookingsBookingListItem": {
        "type": "object",
        "required": [
          "id",
          "start_time",
          "end_time",
          "status",
          "purpose",
          "attendee_count",
          "booking_reference",
          "checked_in",
          "checked_out_at",
          "space",
          "user"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "cancelled",
              "completed",
              "no_show"
            ]
          },
          "purpose": {
            "type": "string",
            "nullable": true
          },
          "attendee_count": {
            "type": "integer",
            "nullable": true
          },
          "booking_reference": {
            "type": "string",
            "nullable": true
          },
          "checked_in": {
            "type": "boolean",
            "description": "True once someone checked in; stays true after check-out."
          },
          "checked_out_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "space": {
            "$ref": "#/components/schemas/BookingsSpaceRef"
          },
          "user": {
            "$ref": "#/components/schemas/BookingsHolder"
          }
        }
      },
      "BookingsBookingDetail": {
        "type": "object",
        "required": [
          "id",
          "start_time",
          "end_time",
          "status",
          "purpose",
          "notes",
          "attendee_count",
          "booking_reference",
          "checked_in",
          "checked_in_at",
          "checked_out_at",
          "cost",
          "space",
          "user"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "cancelled",
              "completed",
              "no_show"
            ]
          },
          "purpose": {
            "type": "string",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "attendee_count": {
            "type": "integer",
            "nullable": true
          },
          "booking_reference": {
            "type": "string",
            "nullable": true
          },
          "checked_in": {
            "type": "boolean",
            "description": "True once someone checked in; stays true after check-out."
          },
          "checked_in_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "checked_out_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cost": {
            "type": "string",
            "nullable": true,
            "description": "A decimal amount rendered as a string (e.g. `\"0.0\"`).",
            "example": "0.0"
          },
          "space": {
            "type": "object",
            "required": [
              "id",
              "name",
              "space_type",
              "location"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "space_type": {
                "type": "string",
                "enum": [
                  "desk",
                  "meeting_room",
                  "parking_spot",
                  "locker",
                  "phone_booth",
                  "av_equipment",
                  "company_vehicle",
                  "gym_equipment"
                ]
              },
              "location": {
                "type": "string",
                "nullable": true,
                "description": "The space's free-text location."
              }
            }
          },
          "user": {
            "$ref": "#/components/schemas/BookingsHolder"
          }
        }
      },
      "BookingsBookingCreated": {
        "type": "object",
        "description": "The booking as a create or a cancel answers it.",
        "required": [
          "id",
          "start_time",
          "end_time",
          "status",
          "booking_reference"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "cancelled",
              "completed",
              "no_show"
            ],
            "description": "`confirmed` after a create, `cancelled` after a cancel."
          },
          "booking_reference": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "BookingsBookingUpdated": {
        "type": "object",
        "description": "The booking as an update answers it.",
        "required": [
          "id",
          "start_time",
          "end_time",
          "status",
          "purpose"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "cancelled",
              "completed",
              "no_show"
            ]
          },
          "purpose": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "BookingsBookingWrite": {
        "type": "object",
        "required": [
          "booking"
        ],
        "properties": {
          "booking": {
            "type": "object",
            "description": "The fields to set. On create, `space_id`, `start_time` and\n`end_time` are needed for a valid booking; on update send only\nwhat changes. Any other key is ignored.\n",
            "properties": {
              "space_id": {
                "type": "integer",
                "description": "One of this business's spaces. An id from another business is dropped (the save then fails \"Space can't be blank\")."
              },
              "start_time": {
                "type": "string",
                "description": "ISO 8601. A naive time (`2026-10-14T14:00`) is read on the room's clock (or in `time_entry_zone`); a time with an offset is taken as sent. An unparseable value is left to the model, which refuses it.",
                "example": "2026-10-14T14:00"
              },
              "end_time": {
                "type": "string",
                "description": "As `start_time`; must be after it.",
                "example": "2026-10-14T15:00"
              },
              "purpose": {
                "type": "string",
                "maxLength": 255
              },
              "notes": {
                "type": "string",
                "maxLength": 2000
              },
              "attendee_count": {
                "type": "integer",
                "minimum": 1,
                "description": "At least 1, and no more than the space's capacity."
              },
              "attendees": {
                "description": "Additional attendee names — an array of strings, or one string with a name per line. Up to 100 names of up to 255 characters.",
                "oneOf": [
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  {
                    "type": "string"
                  }
                ]
              }
            }
          },
          "time_entry_zone": {
            "type": "string",
            "description": "Optional. The time zone a naive `start_time` / `end_time` is read in (a Rails or IANA zone name, e.g. `America/New_York`). An unknown name is ignored and the room's clock is used."
          }
        }
      },
      "BookingsActionResult": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "BookingsValidationError": {
        "description": "The api/v1 error envelope. For `validation_failed`, `message` joins the\nreasons into one sentence and `details.messages` lists each one; the\nother codes carry no `details`.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "object",
                "properties": {
                  "details": {
                    "type": "object",
                    "properties": {
                      "messages": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "Space is not available during the selected time"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "BookingsSpaceListItem": {
        "type": "object",
        "required": [
          "id",
          "name",
          "space_type",
          "capacity",
          "location",
          "building",
          "floor",
          "active",
          "bookable",
          "hourly_rate"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "space_type": {
            "type": "string",
            "enum": [
              "desk",
              "meeting_room",
              "parking_spot",
              "locker",
              "phone_booth",
              "av_equipment",
              "company_vehicle",
              "gym_equipment"
            ]
          },
          "capacity": {
            "type": "integer",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "The space's free-text location."
          },
          "building": {
            "type": "string",
            "nullable": true
          },
          "floor": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean"
          },
          "bookable": {
            "type": "boolean"
          },
          "hourly_rate": {
            "type": "string",
            "nullable": true,
            "description": "A decimal amount rendered as a string (e.g. `\"0.0\"`).",
            "example": "0.0"
          }
        }
      },
      "BookingsSpace": {
        "type": "object",
        "required": [
          "id",
          "name",
          "space_type",
          "capacity",
          "location",
          "building",
          "floor",
          "description",
          "location_id",
          "active",
          "bookable",
          "hourly_rate",
          "amenities",
          "equipment"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "space_type": {
            "type": "string",
            "enum": [
              "desk",
              "meeting_room",
              "parking_spot",
              "locker",
              "phone_booth",
              "av_equipment",
              "company_vehicle",
              "gym_equipment"
            ]
          },
          "capacity": {
            "type": "integer",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "The space's free-text location."
          },
          "building": {
            "type": "string",
            "nullable": true
          },
          "floor": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "The business location (place) the space belongs to, when set."
          },
          "active": {
            "type": "boolean"
          },
          "bookable": {
            "type": "boolean"
          },
          "hourly_rate": {
            "type": "string",
            "nullable": true,
            "description": "A decimal amount rendered as a string (e.g. `\"0.0\"`).",
            "example": "0.0"
          },
          "amenities": {
            "type": "array",
            "description": "Free-form amenity entries (typically strings).",
            "items": {}
          },
          "equipment": {
            "type": "array",
            "description": "Free-form equipment entries (typically strings).",
            "items": {}
          }
        }
      },
      "BrandingTheme": {
        "type": "object",
        "required": [
          "theme",
          "business_id"
        ],
        "properties": {
          "theme": {
            "type": "object",
            "required": [
              "tokens",
              "branding",
              "brand_fonts",
              "palette"
            ],
            "properties": {
              "tokens": {
                "type": "object",
                "description": "Every theme token, defaults overlaid with the tenant's saved values. Additive over time — treat unknown keys as forward compatibility.",
                "additionalProperties": true,
                "properties": {
                  "brand_primary": {
                    "type": "string",
                    "example": "#3066b3"
                  },
                  "font_heading": {
                    "type": "string"
                  },
                  "font_body": {
                    "type": "string"
                  },
                  "font_size_base": {
                    "type": "string",
                    "example": "15px"
                  },
                  "text_primary": {
                    "type": "string"
                  },
                  "text_muted": {
                    "type": "string"
                  },
                  "heading_color": {
                    "type": "string"
                  },
                  "page_bg": {
                    "type": "string"
                  },
                  "card_bg": {
                    "type": "string"
                  },
                  "modal_bg": {
                    "type": "string"
                  },
                  "navbar_bg": {
                    "type": "string"
                  },
                  "navbar_text": {
                    "type": "string",
                    "description": "Derived from `navbar_bg` when the admin left it unset."
                  },
                  "sidebar_text": {
                    "type": "string"
                  },
                  "sidebar_rail_bg": {
                    "type": "string"
                  },
                  "sidebar_rail_active_bg": {
                    "type": "string"
                  },
                  "border_color": {
                    "type": "string"
                  },
                  "border_radius": {
                    "type": "string"
                  },
                  "shadow_color": {
                    "type": "string"
                  },
                  "style_family": {
                    "type": "string",
                    "description": "The style family, which owns corner radius, card style and density."
                  }
                }
              },
              "branding": {
                "type": "object",
                "properties": {
                  "logo_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "favicon_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "sidebar_logo_bg_color": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "custom_css": {
                "type": "string",
                "nullable": true,
                "description": "The tenant's raw custom stylesheet, kept apart from the tokens because it is not a CSS variable."
              },
              "brand_fonts": {
                "type": "array",
                "description": "The uploaded brand fonts the font tokens name, with their files' URLs on this origin. Empty when the tokens name none.",
                "items": {
                  "type": "object",
                  "properties": {
                    "css_family": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "faces": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "url": {
                            "type": "string"
                          },
                          "weight": {
                            "type": "integer",
                            "example": 400
                          },
                          "style": {
                            "type": "string",
                            "enum": [
                              "normal",
                              "italic"
                            ]
                          },
                          "format": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              },
              "palette": {
                "type": "array",
                "description": "Every colour the company's colour choosers offer — brand, accent, the colours it added and the ten named ones — each with the fill and the text colour that reads on it, and the colour as a mark on a card in the light and dark themes. A record that stores a colour stores its `key`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "group": {
                      "type": "string"
                    },
                    "color": {
                      "type": "string"
                    },
                    "fill": {
                      "type": "string"
                    },
                    "ink": {
                      "type": "string"
                    },
                    "mark": {
                      "type": "string"
                    },
                    "mark_dark": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "business_id": {
            "type": "integer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the theme (or, when it was never saved, the business) last changed."
          }
        }
      },
      "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
          }
        }
      },
      "BroadcastNamesHidden": {
        "type": "object",
        "description": "Present only when a works council hides names (General Settings ›\nCountries; Knowledge Layer X3): the people this list leaves out —\ncounted, never named. Absent when nobody is hidden.\n\nWhile the audience's hidden group is smaller than the company's\nsmallest reportable group (Admin › Compliance › Retention & Legal\nHolds), how many of them a list of ONE status leaves out would say\nwhat those few people did: `count` and `group_count` are then null,\n`fewer_than` is that smallest group (\"a few\"), and the report's\nstatus counts (views, acknowledgements) count only the named people.\n\nBelow that smallest group no count is given at all (#4546): when the\nhidden group — or the people this one list leaves out — are fewer than\nit, `count` and `group_count` are null and `fewer_than` is the\nsmallest group (\"a few\").\nThe same shape is used by every read and acknowledgement list of the\nAPI (feed viewers and acknowledgements, feed analytics, wiki viewers,\nchat read reports, the team outstanding view).\n",
        "required": [
          "reason"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "nullable": true,
            "description": "How many people this list leaves out; null while `fewer_than` is set."
          },
          "group_count": {
            "type": "integer",
            "nullable": true,
            "description": "Null since #4546: a hidden group under the company's smallest group\nis \"a few\" (`fewer_than`). Kept for older clients; never broken down.\n"
          },
          "fewer_than": {
            "type": "integer",
            "description": "Present only when `count` (and `group_count`) are null because the\npeople left out are fewer than the company's smallest group: that\nnumber. Show \"a few\".\n"
          },
          "reason": {
            "type": "string",
            "example": "Names are hidden for people in Germany (works council)."
          }
        }
      },
      "PlacementEndResult": {
        "type": "object",
        "description": "The answer of an \"End now\" (`…/placements/{placement_id}/end_now`).",
        "properties": {
          "ended": {
            "type": "boolean",
            "example": true
          },
          "placement_id": {
            "type": "integer"
          },
          "next_placement_id": {
            "type": "integer",
            "nullable": true,
            "description": "The booking queued behind it that starts now (queued places such as Top of Home only), else null."
          },
          "message": {
            "type": "string",
            "example": "Ended — it's off Top of Home now. “Holiday hours” starts there now."
          }
        }
      },
      "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\nA published broadcast reads in the recipient's language when theirs\ndiffers (never for the author): `title` / `description` are then the\ntranslation, `translated` is true and `original_title` /\n`original_description` carry what was written.\n",
        "required": [
          "id",
          "title",
          "description",
          "status",
          "is_critical",
          "require_acknowledgment",
          "viewed",
          "acknowledged",
          "can_acknowledge",
          "created_by",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "On a translation, an unchecked machine translation ends with a short\n\"Machine translation.\" note in the reader's language; a checked one\ncarries no note.\n"
          },
          "translated": {
            "type": "boolean",
            "description": "Whether `title` / `description` are a translation for the caller."
          },
          "translation_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "checked",
              "ai_draft",
              null
            ],
            "description": "The one reader label's facts (Knowledge Layer P6): `checked` — a\nperson checked the translation (\"Translated from English · Checked\nby …\", `translation_checked_by` names them); `ai_draft` — AI\ntranslated it and nobody has checked it yet (\"Translated by AI — not\nyet checked\"). Null when the caller reads the original.\n"
          },
          "translation_checked_by": {
            "type": "string",
            "nullable": true,
            "description": "Who checked the translation — only when `translation_status` is `checked`."
          },
          "original_title": {
            "type": "string",
            "nullable": true,
            "description": "The title as written — only when `translated`."
          },
          "original_description": {
            "type": "string",
            "nullable": true,
            "description": "The description as written — only when `translated`."
          },
          "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."
          },
          "can_acknowledge": {
            "type": "boolean",
            "description": "Whether POST /broadcasts/{id}/acknowledge would succeed for the caller now: the broadcast is published, requires acknowledgment, was SENT to the caller, and the caller has not acknowledged it yet. Key the Acknowledge button on this — never on `require_acknowledgment && !acknowledged`: this list also holds broadcasts the caller authored, and the acknowledge endpoint answers 403 `not_a_recipient` to anyone the broadcast was not sent to."
          },
          "you_owe_acknowledgement": {
            "type": "boolean",
            "description": "The same answer as `can_acknowledge`, named for the reader: the caller still owes this broadcast an acknowledgement. The web Broadcasts list draws a \"You haven't acknowledged this\" badge on such a row."
          },
          "acknowledgement": {
            "type": "object",
            "description": "What the web list draws about acknowledging, for the caller. `badge` and `action` are its words, set only when `you_owe` is true. The action OPENS the broadcast — acknowledging happens on the detail screen, after reading — never a POST from a list row.",
            "properties": {
              "required": {
                "type": "boolean",
                "description": "The broadcast asks its recipients to acknowledge it."
              },
              "you_owe": {
                "type": "boolean",
                "description": "Same as `you_owe_acknowledgement`."
              },
              "badge": {
                "type": "string",
                "nullable": true,
                "example": "You haven't acknowledged this"
              },
              "action": {
                "type": "string",
                "nullable": true,
                "example": "Read and acknowledge"
              }
            }
          },
          "created_by": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Draw the \"AI-assisted\" label (Knowledge Layer E6). Resolved for the caller: true only when the broadcast was written with AI help and the company shows that to readers (Communications settings), or the caller is its sender, the delegate who sent it or a manager of it."
          },
          "sent_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BroadcastUser"
              }
            ],
            "nullable": true,
            "description": "The delegate who sent this broadcast as `created_by` (\"Sent by … on … behalf\"), under the same caller-resolved rule; null when there is none or the caller may not be told."
          },
          "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"
          },
          "translated": {
            "type": "boolean",
            "description": "Same as `BroadcastSummary.translated`."
          },
          "translation_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "checked",
              "ai_draft",
              null
            ],
            "description": "Same as `BroadcastSummary.translation_status` (Knowledge Layer P6)."
          },
          "translation_checked_by": {
            "type": "string",
            "nullable": true
          },
          "original_title": {
            "type": "string",
            "nullable": true
          },
          "original_description": {
            "type": "string",
            "nullable": true
          },
          "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)."
          },
          "can_acknowledge": {
            "type": "boolean",
            "description": "Whether POST /broadcasts/{id}/acknowledge would succeed for the caller now: published, acknowledgment-required, SENT to the caller, and not yet acknowledged by them. Key the Acknowledge button on this — the endpoint answers 403 `not_a_recipient` to an author, admin or manager who can open the broadcast but was never sent it."
          },
          "you_owe_acknowledgement": {
            "type": "boolean",
            "description": "The same answer as `can_acknowledge`, named for the reader: the caller still owes this broadcast an acknowledgement. The web Broadcasts list draws a \"You haven't acknowledged this\" badge on such a row."
          },
          "acknowledgement": {
            "type": "object",
            "description": "What the web list draws about acknowledging, for the caller. `badge` and `action` are its words, set only when `you_owe` is true. The action OPENS the broadcast — acknowledging happens on the detail screen, after reading — never a POST from a list row.",
            "properties": {
              "required": {
                "type": "boolean",
                "description": "The broadcast asks its recipients to acknowledge it."
              },
              "you_owe": {
                "type": "boolean",
                "description": "Same as `you_owe_acknowledgement`."
              },
              "badge": {
                "type": "string",
                "nullable": true,
                "example": "You haven't acknowledged this"
              },
              "action": {
                "type": "string",
                "nullable": true,
                "example": "Read and acknowledge"
              }
            }
          },
          "created_by": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Draw the \"AI-assisted\" label (Knowledge Layer E6). Resolved for the caller: true only when the broadcast was written with AI help and the company shows that to readers (Communications settings), or the caller is its sender, the delegate who sent it or a manager of it."
          },
          "sent_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BroadcastUser"
              }
            ],
            "nullable": true,
            "description": "The delegate who sent this broadcast as `created_by` (\"Sent by … on … behalf\"), under the same caller-resolved rule; null when there is none or the caller may not be told."
          },
          "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) and archive only — NOT by\nGET /broadcasts/{id}, whose detail screen uses `unique_view_count`\ninstead. On a published broadcast whose works-council group is\nunder the company's smallest group, the named viewers only (see\n`names_hidden`).\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"
          },
          "campaign": {
            "nullable": true,
            "allOf": [
              {
                "type": "object",
                "description": "A Communications campaign as a composer draws it (the Campaign field).",
                "required": [
                  "id",
                  "name",
                  "status",
                  "kickoff_at",
                  "ended_at",
                  "open",
                  "can_change"
                ],
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "active",
                      "completed",
                      "canceled"
                    ],
                    "description": "Follows the campaign's dates (a Draft turns Active on its kickoff date, an Active one Completed the day after it ends), unless a person set it."
                  },
                  "kickoff_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "ended_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "open": {
                    "type": "boolean",
                    "description": "Draft or Active — it can take new messages."
                  },
                  "can_change": {
                    "type": "boolean",
                    "description": "Whether the caller may take a message out of it (false for a token without `write:broadcasts`). False: show its name instead of a choice, as the web does."
                  }
                }
              }
            ],
            "description": "The Communications campaign the broadcast is in — for its author or\na caller who may change that campaign (otherwise `null`, as for one\nin no campaign); `can_change` is false for a token without\n`write:broadcasts`. Returned by GET /broadcasts/{id} and the\ncompose responses.\n"
          },
          "attachments": {
            "type": "array",
            "description": "Broadcast file attachments (DriveItem media).",
            "items": {
              "$ref": "#/components/schemas/BroadcastAttachment"
            }
          },
          "hero_image": {
            "type": "object",
            "nullable": true,
            "description": "The broadcast's cover picture — the one the web draws above the\nmessage. Null when it has none. `url` is signed for one hour;\nrefetch the broadcast when it lapses. `width` / `height` are the\npicture's own pixels (null until it has been analysed) — draw it\nat that shape, never cropped.\n",
            "properties": {
              "url": {
                "type": "string"
              },
              "alt": {
                "type": "string",
                "description": "The broadcast's title, as the web uses it."
              },
              "width": {
                "type": "integer",
                "nullable": true
              },
              "height": {
                "type": "integer",
                "nullable": true
              }
            }
          },
          "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 — only the named\nviewers while `names_hidden` is present (the audience's\nworks-council group is under the company's smallest group).\n"
          },
          "names_hidden": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BroadcastNamesHidden"
              }
            ],
            "description": "GET /broadcasts/{id} only, and only while the audience's\nworks-council group is under the company's smallest group\n(Knowledge Layer X3): `count` and `group_count` null, `fewer_than` set. The\nview and acknowledgement counts on this node then count only the\nnamed people, as the viewers and acknowledgements lists do.\n"
          },
          "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. An archived broadcast keeps its thread with comments closed — key the composer on `can_comment`. Returned by GET /broadcasts/{id} only."
          },
          "can_comment": {
            "type": "boolean",
            "description": "Whether the caller may write a comment or reply now — published, commenting enabled, and the `write:broadcasts` scope (what POST /broadcasts/{id}/comments answers). 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\nstats; only the named ones while `names_hidden` is present\n(`total_recipients` stays the whole audience, as\nGET /broadcasts/{id}/acknowledgements' meta does).\n"
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the CALLING user acknowledged (null if they haven't; present when require_acknowledgment)."
          },
          "requested_action": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CommsRequestedAction"
              }
            ],
            "nullable": true,
            "description": "The one thing this broadcast asks THIS reader to do, as the web\nbroadcast page draws it. `null` unless it was sent to the caller\nwith one. Returned by GET /broadcasts/{id} only.\n"
          }
        }
      },
      "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"
          },
          "parent_id": {
            "type": "integer",
            "nullable": true,
            "description": "The top-level comment this reply answers; null on a top-level comment."
          },
          "body": {
            "type": "string",
            "description": "The comment's words. Each @mention is the platform token\n`@[Name](mention:<user id>)`, as in post comments — draw it as the\nperson's name.\n"
          },
          "mentioned_user_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The people this comment mentions (each may open the broadcast)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the words were last edited after posting; null when never."
          },
          "held_for_review": {
            "type": "boolean",
            "description": "True while the comment waits for a moderator. Only its author is\nsent such a comment: show it as waiting, with no reply or reaction.\n"
          },
          "author": {
            "$ref": "#/components/schemas/BroadcastUser"
          },
          "replies": {
            "type": "array",
            "description": "On the list reads (GET /broadcasts/{id}/comments and\n`recent_comments`) only: this top-level comment's visible replies,\noldest first, each in this same shape without `replies`.\n",
            "items": {
              "type": "object"
            }
          },
          "attachments": {
            "type": "array",
            "description": "This comment's file attachments (DriveItem media).",
            "items": {
              "$ref": "#/components/schemas/BroadcastAttachment"
            }
          },
          "reader_translation": {
            "type": "object",
            "nullable": true,
            "description": "The comment in the caller's own language, as the broadcast page\nshows it. `null` when it reads as written — the caller's own\ncomment, one already in their language, or a company that does not\ntranslate for readers — and on the create / update responses.\n`body` above is always the original, for \"Show original\".\n",
            "properties": {
              "language": {
                "type": "string",
                "description": "The caller's language code."
              },
              "state": {
                "type": "string",
                "enum": [
                  "translated",
                  "pending",
                  "awaiting_review",
                  "failed"
                ],
                "description": "`pending` means the translation is queued — the next read of\nthe thread carries it.\n"
              },
              "body": {
                "type": "string",
                "nullable": true,
                "description": "The translated words (mention tokens kept as in `body`); present only when `state` is `translated`."
              },
              "label": {
                "type": "object",
                "nullable": true,
                "description": "The facts for the \"Translated by AI · Show original\" line; present only when `state` is `translated`.",
                "properties": {
                  "status": {
                    "type": "string"
                  },
                  "checked_by": {
                    "type": "string",
                    "nullable": true
                  },
                  "source_language": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the CALLING user can edit this comment — a company admin or a Broadcast / Communications app admin, or its author for 15 minutes after posting while Communications allows editing posts and comments. The comment must be on a published broadcast that takes comments."
          },
          "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, a\ncompany admin, or a Broadcast / Communications app admin, and only\nwhen the token holds `destructive:broadcasts`.\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"
              }
            }
          }
        }
      },
      "BulkOperationRefusal": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "BulkOperationItems": {
        "type": "array",
        "minItems": 1,
        "description": "The records to write. A create item carries the record's attributes; an update item its `id` and the attributes to change; a delete item its `id`. A shift item takes `location_id`, `scheduling_group_id`, `title`, `start_time`, `end_time`, `required_staff_count`, `shift_type`, `notes` and `assignments` (each `user_id` and `status`); an assignment item `shift_id`, `user_id`, `status`, `assignment_type` and, on update, `check_in`, `check_out` and `hours_worked`.",
        "items": {
          "type": "object",
          "additionalProperties": true
        }
      },
      "BulkOperationOptions": {
        "type": "object",
        "properties": {
          "validation_mode": {
            "type": "string",
            "enum": [
              "strict",
              "lenient"
            ],
            "default": "strict",
            "description": "`strict` stops the operation at the first bad item; `lenient` records it and continues."
          },
          "conflict_resolution": {
            "type": "string",
            "enum": [
              "skip",
              "overwrite",
              "fail"
            ],
            "default": "skip",
            "description": "What a create does when the record already exists. `overwrite` can make the request a final act — see the scopes above."
          }
        }
      },
      "BulkOperationTypedRequest": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "operation_type": {
            "type": "string",
            "enum": [
              "create",
              "update",
              "delete"
            ],
            "default": "create"
          },
          "items": {
            "$ref": "#/components/schemas/BulkOperationItems"
          },
          "options": {
            "$ref": "#/components/schemas/BulkOperationOptions"
          }
        }
      },
      "BulkOperationSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer",
            "nullable": true
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "data_type": {
            "type": "string"
          },
          "operation_type": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "total_items": {
            "type": "integer"
          },
          "processed_items": {
            "type": "integer"
          },
          "successful_items": {
            "type": "integer"
          },
          "failed_items": {
            "type": "integer"
          },
          "error_messages": {},
          "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"
          }
        }
      },
      "ChatThreadFollow": {
        "type": "object",
        "properties": {
          "root_id": {
            "type": "integer"
          },
          "following": {
            "type": "boolean"
          }
        }
      },
      "ChatMessageReminder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "remind_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the reminder is due."
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "snoozed"
            ]
          },
          "room_id": {
            "type": "integer"
          },
          "message_id": {
            "type": "integer"
          },
          "available": {
            "type": "boolean",
            "description": "False when the caller can no longer open the message — no notice will go out",
            "and `message` is null.": null
          },
          "message": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatMessage"
              }
            ]
          }
        }
      },
      "ChatMessageReminderSet": {
        "type": "object",
        "properties": {
          "reminder": {
            "$ref": "#/components/schemas/ChatMessageReminder"
          },
          "moved": {
            "type": "boolean",
            "description": "True when the caller's open reminder on this message was moved to the new time instead of a second one being set (the answer is then 200)."
          },
          "message": {
            "type": "string",
            "description": "The sentence to show, in the caller's language and time zone — the web's own — e.g. \"I'll remind you in 20 minutes.\" or \"I'll remind you today at 8:29 AM.\"; when `moved`, it starts by saying the earlier reminder was moved."
          },
          "confirmation": {
            "type": "string",
            "description": "The same sentence as `message`",
            "under the name this answer first used.": null
          }
        }
      },
      "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
          },
          "greeting": {
            "type": "string",
            "nullable": true,
            "description": "The assistant's opening line (Ask AI › Guidelines › Assistant); null ⇒ the client's own welcome."
          },
          "history_tip": {
            "type": "string",
            "nullable": true
          },
          "history_search": {
            "type": "boolean",
            "description": "ask_ai_account only — retrieval-grounded answers are live for this tenant."
          }
        }
      },
      "ChatMemberImportPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "description": "What tells namesakes apart (job title, location…) — only what the caller's directory shows; never an email the directory hides."
          }
        }
      },
      "ChatMemberImport": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "The person's place in the list."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "ready",
                    "pick",
                    "not_found",
                    "unavailable"
                  ]
                },
                "given": {
                  "type": "object",
                  "description": "What the list said about them (their own words; the email is the list's, not the directory's).",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    },
                    "employee_id": {
                      "type": "string"
                    },
                    "job_title": {
                      "type": "string"
                    },
                    "department": {
                      "type": "string"
                    },
                    "location": {
                      "type": "string"
                    },
                    "list": {
                      "type": "string",
                      "description": "Which of several lists in the content they are on (see `lists`)."
                    }
                  }
                },
                "match": {
                  "type": "string",
                  "nullable": true,
                  "enum": [
                    "email",
                    "employee_id",
                    "name",
                    "first_name",
                    "name_from_email",
                    "close",
                    "conversation",
                    null
                  ],
                  "description": "How they were found — `conversation`: copied from the room `from_room_id` named. A first_name, name_from_email or close match is never ready — a pick, or not_found when too many share the first name."
                },
                "person": {
                  "$ref": "#/components/schemas/ChatMemberImportPerson"
                },
                "reason_code": {
                  "type": "string",
                  "description": "unavailable rows (and unavailable candidates) — you / already_member (only where the caller may see the member list) / listed_twice (a candidate already on the list) / deactivated / chat_not_open / information_barrier / barrier_room / barrier_list"
                },
                "reason": {
                  "type": "string",
                  "description": "The sentence to show beside the row",
                  "in the caller's language.": null
                },
                "candidates": {
                  "type": "array",
                  "description": "pick rows (and an unavailable row whose candidates all are) — choose one; best first (those the list's own title, department or location point at come first, `hinted`).",
                  "items": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ChatMemberImportPerson"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "available": {
                            "type": "boolean"
                          },
                          "reason_code": {
                            "type": "string",
                            "nullable": true
                          },
                          "reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "hinted": {
                            "type": "boolean"
                          }
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "ready": {
                "type": "integer"
              },
              "pick": {
                "type": "integer"
              },
              "not_found": {
                "type": "integer"
              },
              "unavailable": {
                "type": "integer"
              },
              "duplicates": {
                "type": "integer",
                "description": "People listed twice (shown once)."
              },
              "found": {
                "type": "integer",
                "description": "People the content held."
              },
              "read": {
                "type": "integer",
                "description": "People read (at most `max_people`)."
              }
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "The content held more than `max_people` people; the first ones were read."
          },
          "max_people": {
            "type": "integer"
          },
          "email_shown": {
            "type": "boolean",
            "description": "Whether the caller's directory shows email — when false, emails in the list are not used to find people."
          },
          "sends": {
            "type": "object",
            "nullable": true,
            "description": "What confirming will SEND, so the review is an honest last look before anyone is told (null when it sends nothing). Built from this door's own rules, never typed beside them.",
            "properties": {
              "sentence": {
                "type": "string",
                "description": "The sentence to show",
                "in the caller's language.": null
              },
              "now": {
                "type": "integer",
                "description": "How many of the ready people are told straight away."
              },
              "later": {
                "type": "integer",
                "description": "How many are told later",
                "once something else happens first.": null
              }
            }
          },
          "lists": {
            "type": "array",
            "description": "When the content holds several lists (teams, groups), each one's name and how many people are on it — let the person choose.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "list": {
            "type": "object",
            "description": "What the list says about itself.",
            "properties": {
              "name": {
                "type": "string",
                "description": "Its name as written."
              },
              "channel_name": {
                "type": "string",
                "description": "That name as a channel name (lowercase words joined by dashes)."
              },
              "channel_name_error": {
                "type": "string",
                "description": "Set when no channel can take that name (one already has it)."
              },
              "description": {
                "type": "string",
                "description": "Its topic or purpose — a channel's topic."
              },
              "visibility": {
                "type": "string",
                "enum": [
                  "public",
                  "private"
                ]
              },
              "source": {
                "type": "string",
                "description": "What the content is",
                "in a few words.": null
              },
              "copied_from": {
                "type": "string",
                "description": "The group or channel the people were copied from (`from_room_id`), as its members see it named (\"#support\"). Set only for a copy."
              }
            }
          },
          "settings": {
            "type": "object",
            "nullable": true,
            "description": "The list's settings mapped onto Chat's (null for an existing conversation).",
            "properties": {
              "values": {
                "$ref": "#/components/schemas/ChatRoomPermissionCodes"
              },
              "carried": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string",
                      "description": "The list's own question."
                    },
                    "value": {
                      "type": "string",
                      "description": "The list's own answer."
                    },
                    "setting": {
                      "type": "string",
                      "description": "Chat's setting, in the caller's language."
                    },
                    "choice": {
                      "type": "string",
                      "description": "Chat's choice, in the caller's language."
                    }
                  }
                }
              },
              "not_carried": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "photo": {
            "type": "object",
            "nullable": true,
            "description": "The list's own photo, fetched now (null when it has none, or for an existing conversation).",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "ready",
                  "failed"
                ]
              },
              "signed_id": {
                "type": "string",
                "description": "ready — pass as `photo_signed_id` when creating (kept for a day",
                "only for the caller).": null
              },
              "url": {
                "type": "string",
                "description": "ready — a preview link (an hour)."
              },
              "host": {
                "type": "string",
                "description": "Where the link pointed."
              },
              "looks_default": {
                "type": "boolean",
                "description": "The source app's own default picture, by the look of the link — offer it unticked."
              },
              "reason": {
                "type": "string",
                "description": "failed — why it couldn't be used."
              }
            }
          },
          "conversation": {
            "type": "object",
            "properties": {
              "for": {
                "type": "string",
                "enum": [
                  "new",
                  "room"
                ]
              },
              "group_member_limit": {
                "type": "integer",
                "description": "A group holds this many people",
                "the caller included; past it a new conversation must be a private channel.": null
              },
              "can_create_channels": {
                "type": "boolean"
              },
              "direct_messages_enabled": {
                "type": "boolean",
                "description": "One person makes a direct message — false when the tenant switched them off (pick a second person for a group)."
              },
              "room_id": {
                "type": "integer"
              },
              "room_type": {
                "type": "string",
                "enum": [
                  "group",
                  "channel"
                ]
              },
              "member_count": {
                "type": "integer"
              },
              "over_limit_if_all_added": {
                "type": "boolean",
                "description": "Adding every ready person would take this group past the limit — it must become a private channel first (POST /chat/rooms/{id}/convert_to_channel)."
              }
            }
          }
        }
      },
      "ChatChannelAvatarColor": {
        "type": "string",
        "enum": [
          "red",
          "orange",
          "amber",
          "green",
          "teal",
          "cyan",
          "blue",
          "indigo",
          "purple",
          "pink"
        ],
        "example": "teal"
      },
      "ChatChannelAvatar": {
        "type": "object",
        "properties": {
          "emoji": {
            "type": "string",
            "nullable": true,
            "description": "One emoji; null draws the colour alone.",
            "example": "📣"
          },
          "color": {
            "$ref": "#/components/schemas/ChatChannelAvatarColor"
          },
          "source": {
            "type": "string",
            "enum": [
              "auto",
              "chosen"
            ],
            "description": "`auto` — picked from the name (a rename picks again); `chosen` — someone picked it, and it stays."
          }
        }
      },
      "ChatChannelAvatarChoice": {
        "type": "object",
        "required": [
          "color"
        ],
        "description": "Channel only. One of the palette colours and at most one emoji; anything else is 422 `invalid_avatar` and nothing is written.",
        "properties": {
          "emoji": {
            "type": "string",
            "nullable": true,
            "description": "A single emoji; blank for the colour alone."
          },
          "color": {
            "$ref": "#/components/schemas/ChatChannelAvatarColor"
          }
        }
      },
      "ChatMessageEscalation": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "notified",
              "everyone_confirmed",
              "not_delivered",
              "barred",
              "expired"
            ],
            "description": "`pending` until the wait is up; then `notified` (the backup was told), `everyone_confirmed` (nobody was left to name), `not_delivered` (the backup left, or has these notices off — the sender was sent the list), or `barred` (an information barrier now stands between the sender and the backup, so the backup was not told — the sender was sent the list). `expired` — still pending more than 14 days after it was sent (the sweep never reached it: the worker was down, Chat was switched off): nobody will be told any more, so there is nothing to cancel (`can_cancel` false; DELETE …/escalation still takes the backup off)."
          },
          "backup": {
            "nullable": true,
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "escalate_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the backup is told if someone has still not confirmed."
          },
          "escalated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "pending_count": {
            "type": "integer",
            "nullable": true,
            "description": "How many had not confirmed when it was decided."
          },
          "wait_label": {
            "type": "string",
            "nullable": true,
            "example": "2 hours"
          },
          "can_cancel": {
            "type": "boolean"
          }
        }
      },
      "ChatNotificationMirror": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "False while `retired`, whatever was chosen."
          },
          "retired": {
            "type": "boolean",
            "description": "True while the company's \"One interruption per item\" is on: nothing is copied (Needs you carries what needs the person), and PUT answers 409 `retired`."
          },
          "retired_reason": {
            "type": "string",
            "nullable": true,
            "description": "The sentence to show while `retired`."
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The kinds chosen."
          },
          "available": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "description": {
                  "type": "string",
                  "nullable": true
                },
                "selected": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "ChatMessageCard": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "approval",
              "acknowledge",
              "sign",
              "link",
              "notice",
              "open_shift",
              "app_event"
            ],
            "description": "`approval` — an approvals-registry row; `acknowledge` / `sign` — an Inbox ask's one button; `link` — an ask its page finishes; `notice` — a notice to see; `open_shift` / `app_event` — an app's update in a channel (no recipient)."
          },
          "source_key": {
            "type": "string",
            "description": "An approval's queue (leave_requests, timesheets, shift_claims, …); an ask card's source type (Broadcast, Task, …)."
          },
          "record_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "done",
              "gone"
            ],
            "description": "`done` — an ask card its person answered, here or anywhere (\"Acknowledged by you\", \"Signed by you\"). `gone` — decided elsewhere, or no longer waiting on the caller. What happened is in `outcome` / `decided_via` / `gone_reason`."
          },
          "decided_by": {
            "nullable": true,
            "description": "Who decided it — from this card (approved / rejected), or elsewhere (`gone` with an `outcome`). Name them \"you\" when `id` is the caller.",
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "outcome": {
            "type": "string",
            "nullable": true,
            "enum": [
              "approved",
              "rejected",
              null
            ],
            "description": "On a `gone` card, the decision made elsewhere through the approvals registry (the Inbox, the approvals page, an emailed link, Teams, Slack, Ask AI, another person's card) — \"Approved by you, from your Inbox\". Null when it went another way (withdrawn, moved on, decided in the app's own page) or before 2026-10-18."
          },
          "decided_via": {
            "type": "string",
            "nullable": true,
            "enum": [
              "inbox",
              "approvals_page",
              "home",
              "phone",
              "email",
              "teams",
              "slack",
              "ask_ai",
              "chat",
              null
            ],
            "description": "Where that decision was made, when the door said so; null when not known (say who and what only)."
          },
          "gone_reason": {
            "type": "string",
            "nullable": true,
            "description": "On a `gone` card with no `outcome`, the sentence the app closed its Inbox ask with (\"Marcus withdrew this request.\"); null when it gave none — say \"Already handled — this is no longer waiting on you.\""
          },
          "can_approve": {
            "type": "boolean"
          },
          "can_reject": {
            "type": "boolean",
            "description": "False when the queue asks a reason for a decline",
            "or asks before one (a leave request) — see `decline_needs_reason`.": null
          },
          "decline_needs_reason": {
            "type": "boolean",
            "description": "Open `row.deep_link` to decline it with a reason."
          },
          "can_act": {
            "type": "boolean",
            "description": "A card the caller can answer here: draw `row.act_label` as its button (verdict: the kind; `seen` for a notice, `claim` for an open shift)."
          },
          "pending_approval": {
            "type": "boolean",
            "description": "An open-shift card claimed here whose claim waits for a manager (\"Claimed by Dana · waiting for approval\")."
          },
          "act_needs_app": {
            "type": "boolean",
            "description": "An ask card whose button asks for statements first: open `row.deep_link` (\"Open to finish\") instead."
          },
          "row": {
            "type": "object",
            "description": "The approvals registry's row as drawn for the caller. Details, amount and urgency only while pending.",
            "properties": {
              "kind_label": {
                "type": "string"
              },
              "subject": {
                "type": "string"
              },
              "subject_secondary": {
                "type": "string",
                "nullable": true
              },
              "summary": {
                "type": "string",
                "nullable": true
              },
              "details": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "key"
                  ],
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "The line's stable snake_case name — find a line by it, never by `label`, which is reworded and translated (`missing_day_requests` on a timesheet with missing-day requests waiting; `cannot_approve` / `cannot_approve_yet` on a shift claim Approve refuses; `balance`, `shift_conflict`, `coverage_over_limit` / `coverage_warning` on a leave request). A line its source names no key for is keyed by its English label (`due`, `type`), and a line with no label is `note`. Never localized. The Home Pending Approvals card's `decidables[].details` (GET /api/v1/home) carry the same lines."
                    },
                    "count": {
                      "type": "integer",
                      "nullable": true,
                      "description": "What the line counts, where it counts something (`missing_day_requests`: the missing-day requests still waiting)."
                    },
                    "label": {
                      "type": "string",
                      "nullable": true
                    },
                    "value": {
                      "type": "string",
                      "nullable": true
                    },
                    "from": {
                      "type": "string",
                      "nullable": true,
                      "description": "A change's old value (with `to`)."
                    },
                    "to": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "amount": {
                "type": "string",
                "nullable": true
              },
              "urgency_label": {
                "type": "string",
                "nullable": true
              },
              "deep_link": {
                "type": "string",
                "nullable": true,
                "description": "The item's page on the web (a path)."
              },
              "deep_link_label": {
                "type": "string"
              },
              "approve_label": {
                "type": "string"
              },
              "reject_label": {
                "type": "string"
              },
              "act_label": {
                "type": "string",
                "nullable": true,
                "description": "An ask card's button (\"Acknowledge\", \"Sign\"); null on an approval."
              },
              "blocked_reason": {
                "type": "string",
                "nullable": true
              },
              "image_url": {
                "type": "string",
                "nullable": true,
                "description": "A channel card's picture — an award's card picture (1200×630) — at an absolute address on the company's own host. It stops answering when the moment is no longer shared; draw the card without it then."
              },
              "image_alt": {
                "type": "string",
                "nullable": true,
                "description": "The picture's text alternative."
              },
              "people": {
                "type": "array",
                "nullable": true,
                "description": "The day's Celebrations card: the first people celebrated, each with their card picture (`image_url`, as above) when there is one.",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "line": {
                      "type": "string",
                      "nullable": true,
                      "description": "What they are celebrated for (\"5 Year Work Anniversary\", \"Certified: Food Safety\")."
                    },
                    "image_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              },
              "more": {
                "type": "string",
                "nullable": true,
                "description": "The Celebrations card's line naming the rest (\"And 3 more: …\")."
              },
              "more_copy": {
                "type": "object",
                "nullable": true,
                "additionalProperties": true,
                "description": "The same line as a sentence a client can say in its reader's language (a Translation::Sentence hash); a client that does not translate shows `more`."
              }
            }
          }
        }
      },
      "ChatRoomAppEvents": {
        "type": "object",
        "properties": {
          "room_id": {
            "type": "integer"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "open_shifts",
                    "coverage_gaps",
                    "team_recognition",
                    "celebrations"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "description": {
                  "type": "string",
                  "nullable": true,
                  "description": "The line under the name, where it has one (Celebrations)."
                },
                "true": {
                  "type": "boolean"
                }
              }
            }
          },
          "can_change": {
            "type": "boolean"
          }
        }
      },
      "ChatIncomingWebhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "room_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "url_hint": {
            "type": "string",
            "nullable": true,
            "description": "The URL's last four characters (\"…a1b2\")."
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "created_by_active": {
            "type": "boolean",
            "nullable": true,
            "description": "false once the person who added it is no longer a member of the company; null when nobody is recorded."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it last posted."
          },
          "last_refused_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a post to it was last refused (at most one write a minute)."
          },
          "last_refusal": {
            "type": "string",
            "nullable": true,
            "enum": [
              "rate_limited",
              "invalid_payload",
              "no_text",
              "subscription_not_confirmed",
              "invalid_sns_signature",
              "payload_too_large"
            ],
            "description": "Why it was refused."
          },
          "last_refusal_message": {
            "type": "string",
            "nullable": true,
            "description": "The refusal as one sentence, while it is newer than the last post (\"Last refused 5 minutes ago: …\")."
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ChatOnShiftPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "avatar_updated_at": {
            "type": "integer",
            "nullable": true
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "role": {
            "type": "string",
            "nullable": true,
            "example": "Shift Lead",
            "description": "Their job function — the role a shift is staffed by — named for people (a stored key `crew_lead` reads \"Crew Lead\"; typed text is kept), as `GET /chat/on_shift/options` `role_options` labels it. A label to show: `?role=` takes the stored value (`role_options[].value`, `roles`). Null when they have none. A shift has no single role of its own."
          },
          "shift_starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The start of their CURRENT shift: clocked in, the shift they clocked in to; else, of the shifts they are on now, the one most recently begun (on `GET /chat/on_shift`, the one at the asked location or a site beneath it). Show it on the shift's clock (`shift_time_zone`), as the Shifts app does, with the zone's short label when that is not the viewer's (\"8:00 AM – 4:00 PM PDT\"). Null — with `shift_ends_at` and `shift_time_zone` — when the caller may not see this person's shift: they see their own, the people they manage and the people at their own locations; company admins and Shifts app admins see everyone's. Show role and location only then."
          },
          "shift_ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "That shift's end — the next morning for an overnight shift. Null for a walk-in shift, which has no scheduled end (show \"since <start>\"), and when the caller may not see this person's shift (see `shift_starts_at`)."
          },
          "shift_time_zone": {
            "type": "string",
            "nullable": true,
            "example": "America/Los_Angeles",
            "description": "The IANA zone the shift's times are read in — the zone the Shifts app shows the shift in: its site's own zone, else the company's. Present whenever `shift_starts_at` is; null together with it (the caller may not see this person's shift)."
          }
        }
      },
      "ChatLegalHold": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "reason": {
            "type": "string"
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "placed_by": {
            "type": "string",
            "description": "Who placed it (\"System\" for a hold an app placed)."
          },
          "lifted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "lifted_by": {
            "type": "string",
            "nullable": true
          },
          "lift_reason": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ClassifiedListing": {
        "type": "object",
        "description": "A listing as Browse, My Favorites and My Listings list it.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "listing_type": {
            "type": "string",
            "enum": [
              "for_sale",
              "free_giveaway",
              "wanted_request",
              "services_offered"
            ]
          },
          "listing_type_display": {
            "type": "string",
            "example": "For Sale"
          },
          "price": {
            "type": "number",
            "nullable": true,
            "description": "Null on a wanted request, which shows no price."
          },
          "price_display": {
            "type": "string",
            "nullable": true,
            "example": "$250.00"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending_approval",
              "fulfilled",
              "expired",
              "deleted"
            ],
            "description": "An active listing past its expiry date reads `expired`."
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "The category's name."
          },
          "category_id": {
            "type": "integer",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "location_id": {
            "type": "integer",
            "nullable": true
          },
          "poster_name": {
            "type": "string"
          },
          "has_photos": {
            "type": "boolean"
          },
          "views_count": {
            "type": "integer",
            "nullable": true,
            "description": "Null while the app's view counter is off."
          },
          "favorites_count": {
            "type": "integer",
            "nullable": true,
            "description": "Null while the app's Favorites setting is off."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ClassifiedMyListing": {
        "description": "One of the caller's own listings (GET /classified_listings/my_listings).",
        "allOf": [
          {
            "$ref": "#/components/schemas/ClassifiedListing"
          },
          {
            "type": "object",
            "properties": {
              "status_text": {
                "type": "string",
                "description": "The state in words; a `deleted` listing reads Deleted, Removed by moderator or Rejected."
              },
              "removed_by_moderator": {
                "type": "boolean"
              },
              "rejection_reason": {
                "type": "string",
                "nullable": true,
                "description": "The moderator's reason, on a rejected listing."
              },
              "repost_path": {
                "type": "string",
                "nullable": true,
                "description": "The page that posts a fixed copy of a rejected listing, until one is posted."
              }
            }
          }
        ]
      },
      "ClassifiedListingDetail": {
        "description": "One listing in full (GET, POST and PATCH answer this).",
        "allOf": [
          {
            "$ref": "#/components/schemas/ClassifiedListing"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": "string"
              },
              "currency": {
                "type": "string",
                "example": "USD"
              },
              "location_detail": {
                "type": "string",
                "nullable": true
              },
              "contact_method": {
                "type": "string",
                "enum": [
                  "internal_chat",
                  "email",
                  "phone",
                  "text"
                ]
              },
              "contact_method_display": {
                "type": "string"
              },
              "contact_email": {
                "type": "string",
                "description": "Present only when `contact_method` is `email`."
              },
              "contact_phone": {
                "type": "string",
                "description": "Present only when `contact_method` is `phone` or `text`."
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "photo_count": {
                "type": "integer"
              },
              "video_count": {
                "type": "integer"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "days_until_expiry": {
                "type": "integer"
              },
              "favorited_by_me": {
                "type": "boolean"
              },
              "can_edit": {
                "type": "boolean",
                "description": "Whether PATCH /classified_listings/{id} would take this caller's edit."
              },
              "can_delete": {
                "type": "boolean",
                "description": "Whether DELETE /classified_listings/{id} would take this caller's removal."
              },
              "poster_department": {
                "type": "string",
                "nullable": true,
                "description": "Present only when the app shows the poster's department."
              }
            }
          }
        ]
      },
      "ClassifiedListingInput": {
        "type": "object",
        "description": "A listing's fields. On POST, `title`, `description` and `listing_type` are needed, and the app's settings may require a category, a location or a contact detail too; on PATCH send only what changes.",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "listing_type": {
            "type": "string",
            "enum": [
              "for_sale",
              "free_giveaway",
              "wanted_request",
              "services_offered"
            ]
          },
          "classified_category_id": {
            "type": "integer",
            "description": "An id from GET /classified_listings/categories."
          },
          "price": {
            "type": "number",
            "description": "Ignored for a free giveaway or wanted request."
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "location": {
            "type": "string",
            "description": "A pickup or meeting place in words."
          },
          "location_id": {
            "type": "integer",
            "description": "One of the organization's locations."
          },
          "contact_method": {
            "type": "string",
            "enum": [
              "internal_chat",
              "email",
              "phone",
              "text"
            ],
            "description": "How people reach the seller. `phone` and `text` need `contact_phone`; `email` publishes `contact_email`, else the seller's own address."
          },
          "contact_email": {
            "type": "string"
          },
          "contact_phone": {
            "type": "string"
          },
          "tags": {
            "type": "string",
            "description": "Comma-separated, as one string — a list is refused with 422."
          },
          "photos": {
            "type": "array",
            "description": "Image files, in a multipart/form-data request only.",
            "items": {
              "type": "string",
              "format": "binary"
            }
          }
        }
      },
      "ClassifiedCategory": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "icon": {
            "type": "string",
            "description": "A Font Awesome class."
          },
          "color": {
            "type": "string",
            "example": "#007bff"
          },
          "active_listings_count": {
            "type": "integer",
            "description": "How many listings Browse shows in the category."
          },
          "postable": {
            "type": "boolean",
            "description": "False when the app's prohibited categories setting refuses new listings in it."
          }
        }
      },
      "ClassifiedPageMeta": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          }
        }
      },
      "ClassifiedErrors": {
        "type": "object",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ClassifiedsRefusal": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "You cannot delete this listing."
          }
        }
      },
      "CommsHubPagination": {
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total_count",
          "total_pages"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total_count": {
            "type": "integer",
            "example": 3
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          }
        }
      },
      "CommsHubPublication": {
        "type": "object",
        "required": [
          "id",
          "name",
          "slug",
          "description",
          "status",
          "cadence_cron",
          "audience_id",
          "email_channel_enabled",
          "inapp_channel_enabled",
          "inapp_requires_acknowledgement",
          "public_archive_enabled",
          "issues_count_cache"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "The Weekly Mango"
          },
          "slug": {
            "type": "string",
            "example": "the-weekly-mango"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "archived"
            ]
          },
          "cadence_cron": {
            "type": "string",
            "nullable": true,
            "description": "The send cadence as a cron expression; null when the publication has no fixed cadence.",
            "example": "0 9 * * 1"
          },
          "audience_id": {
            "type": "integer",
            "nullable": true,
            "description": "The publication's default audience (`/comms_hub/audiences/{id}`)."
          },
          "email_channel_enabled": {
            "type": "boolean"
          },
          "inapp_channel_enabled": {
            "type": "boolean"
          },
          "inapp_requires_acknowledgement": {
            "type": "boolean",
            "description": "Whether the in-app Must-Read copy asks readers to acknowledge it (and so sends reminders and manager escalation)."
          },
          "public_archive_enabled": {
            "type": "boolean"
          },
          "issues_count_cache": {
            "type": "integer",
            "description": "Cached number of issues."
          }
        }
      },
      "CommsHubIssue": {
        "type": "object",
        "required": [
          "id",
          "title",
          "slug",
          "preview_text",
          "status",
          "sent_at",
          "corrected_at",
          "scheduled_at",
          "audience_id",
          "custom_audience",
          "topics",
          "blocks_count",
          "ai_assisted",
          "audience_deleted"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "description": "The subject line — translated when `language` is not `en`."
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "preview_text": {
            "type": "string",
            "nullable": true,
            "description": "The inbox preview line — translated when `language` is not `en`."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_review",
              "scheduled",
              "sent",
              "archived",
              "rejected"
            ]
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "corrected_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the sent issue's online version (its page, in-app post and public archive) was last corrected; the web marks it \"Updated\" with this date. Emails already delivered keep what was sent. Null when never corrected."
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "audience_id": {
            "type": "integer",
            "nullable": true,
            "description": "The issue's own saved audience; null when it uses the publication's, when `custom_audience` is true, or when the audience was deleted after the issue went out (`audience_deleted`)."
          },
          "audience_deleted": {
            "type": "boolean",
            "description": "True when the issue was sent to a saved audience that has since been deleted."
          },
          "custom_audience": {
            "type": "boolean",
            "description": "True when the issue goes to a selection made in Review & send — several saved audiences, groups or people — rather than one saved audience or the publication's."
          },
          "topics": {
            "type": "array",
            "description": "The issue's Topics (normalized topic names) — the chips shown under it, carried onto its in-app post.",
            "items": {
              "type": "string"
            },
            "example": [
              "company-update",
              "internal-comms"
            ]
          },
          "blocks_count": {
            "type": "integer"
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "True when the issue was written with AI help AND the caller may be told so — the company shows that mark to readers, or the caller is the issue's author, the publication's owner or a Communications app admin. Never the raw stored flag."
          },
          "language": {
            "type": "string",
            "description": "Present only for a sent issue. The language `title`, `preview_text` and `blocks` are in (`en` = the original).",
            "example": "es"
          },
          "available_languages": {
            "type": "array",
            "description": "Present only for a sent issue. `en` plus every language it can be read in: on the list, the versions frozen for the send; for one issue, also every language the company offers its readers, which AI translates on request (`reviewed_languages` says which a person checked).",
            "items": {
              "type": "string"
            },
            "example": [
              "en",
              "es"
            ]
          },
          "reviewed_languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present beside `available_languages`: the languages a person checked — the version frozen for the send, or a reader edition a person approved for the issue as it reads now. Every other available language is translated by AI on request; mark the two apart as the web picker does (\"Spanish · reviewed\").",
            "example": [
              "es"
            ]
          },
          "source_fingerprint": {
            "type": "string",
            "nullable": true,
            "description": "Present only for a sent issue. Fingerprint of the delivered content, stable across languages."
          }
        }
      },
      "CommsHubIssueDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CommsHubIssue"
          },
          {
            "type": "object",
            "required": [
              "blocks",
              "campaign",
              "requested_action"
            ],
            "properties": {
              "requested_action": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CommsRequestedAction"
                  }
                ],
                "nullable": true,
                "description": "The one thing this issue asks THIS reader to do, as the web issue page draws it. `null` unless it was sent to the caller with one."
              },
              "translation": {
                "type": "string",
                "enum": [
                  "original",
                  "reviewed",
                  "ai",
                  "pending",
                  "unavailable"
                ],
                "description": "Present only for a sent issue that carries its delivered document. `reviewed` — a person approved this language; `ai` — translated by AI, not yet checked (say so to the reader); `pending` — being translated, the original is returned meanwhile (ask again in a minute); `unavailable` — not offered in that language, the original is returned."
              },
              "checked_before_correction": {
                "type": "boolean",
                "description": "Present only beside `translation`. True when the language version returned was checked by a person before the issue's latest correction (`corrected_at`), so it keeps the words as they were then — say so and offer the original (`lang=en`), as the web reader page does. False for the original, an AI translation, or a version checked after the correction."
              },
              "campaign": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CommsHubCampaign"
                  }
                ],
                "nullable": true,
                "description": "The campaign the issue is in — for a caller who manages the issue or may change that campaign (otherwise null, as for an issue in no campaign); `can_change` is false for a token without `write:broadcasts`."
              },
              "blocks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CommsHubIssueBlock"
                }
              }
            }
          }
        ]
      },
      "CommsHubCampaign": {
        "type": "object",
        "description": "A Communications campaign as a composer draws it (the Campaign field).",
        "required": [
          "id",
          "name",
          "status",
          "kickoff_at",
          "ended_at",
          "open",
          "can_change"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "canceled"
            ],
            "description": "Follows the campaign's dates (a Draft turns Active on its kickoff date, an Active one Completed the day after it ends), unless a person set it."
          },
          "kickoff_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "open": {
            "type": "boolean",
            "description": "Draft or Active — it can take new messages."
          },
          "can_change": {
            "type": "boolean",
            "description": "Whether the caller may take a message out of it (false for a token without `write:broadcasts`). False: show its name instead of a choice, as the web does."
          }
        }
      },
      "CommsHubCampaignDetail": {
        "type": "object",
        "description": "One campaign as its web page shows it (GET /comms_hub/campaigns/{id}).",
        "required": [
          "id",
          "name",
          "status",
          "kickoff_at",
          "ended_at",
          "open",
          "can_change",
          "description",
          "theme",
          "owner",
          "pieces_count",
          "sent_count",
          "follows",
          "followable_triggers",
          "goals"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "canceled"
            ]
          },
          "kickoff_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "open": {
            "type": "boolean",
            "description": "Draft or Active — it can take new messages and follow a trigger."
          },
          "can_change": {
            "type": "boolean",
            "description": "Whether the caller may change what is in it (false for a token without `write:broadcasts`)."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "theme": {
            "type": "string",
            "nullable": true
          },
          "owner": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "pieces_count": {
            "type": "integer",
            "description": "Pieces in the campaign, sent or not — a followed trigger counts once."
          },
          "sent_count": {
            "type": "integer",
            "description": "Pieces that went out — a followed trigger counts once, when it has sent any."
          },
          "follows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommsHubCampaignFollow"
            }
          },
          "followable_triggers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "label",
                "enabled"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "label": {
                  "type": "string",
                  "description": "The trigger's name on the Triggers page."
                },
                "enabled": {
                  "type": "boolean",
                  "description": "Whether the trigger is switched on (an off one sends nothing new)."
                }
              }
            }
          },
          "goals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommsHubCampaignGoal"
            }
          }
        }
      },
      "CommsHubCampaignFollow": {
        "type": "object",
        "description": "A trigger the campaign follows — ONE piece, however many triggered messages it sent.",
        "required": [
          "trigger_id",
          "label",
          "sent_in_campaign",
          "from",
          "until",
          "followed_at"
        ],
        "properties": {
          "trigger_id": {
            "type": "integer"
          },
          "label": {
            "type": "string",
            "description": "The trigger's name on the Triggers page (\"Week 1 check-in\")."
          },
          "sent_in_campaign": {
            "type": "integer",
            "description": "Its triggered messages in the campaign that were sent."
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "The first day its messages are taken from (the kickoff date, or the day the campaign was created)."
          },
          "until": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The last day (the campaign's end date); null — no end date."
          },
          "followed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CommsHubCampaignFollowResult": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "follow",
              "added",
              "changed"
            ],
            "properties": {
              "follow": {
                "$ref": "#/components/schemas/CommsHubCampaignFollow"
              },
              "added": {
                "type": "integer",
                "description": "Triggered messages already sent in the campaign's dates that joined now."
              },
              "changed": {
                "type": "boolean",
                "description": "False when it already followed that trigger."
              }
            }
          }
        }
      },
      "CommsHubCampaignGoal": {
        "type": "object",
        "required": [
          "key",
          "label",
          "target",
          "value",
          "measured",
          "reached",
          "note",
          "summary",
          "breakdown"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "opens",
              "clicks",
              "acknowledgments",
              "completions",
              "completions_pct"
            ]
          },
          "label": {
            "type": "string"
          },
          "target": {
            "type": "integer"
          },
          "value": {
            "type": "integer",
            "nullable": true,
            "description": "The goal's number; null when it has none (`note` says why)."
          },
          "measured": {
            "type": "boolean"
          },
          "reached": {
            "type": "boolean"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "How the number is put together (\"Added up across pieces …\")."
          },
          "breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommsHubCampaignGoalPiece"
            }
          }
        }
      },
      "CommsHubCampaignGoalPiece": {
        "type": "object",
        "description": "One sent piece's share of a goal: a message, or a followed trigger's triggered messages (`trigger_id`, `sent` of them). The counted pieces' values add up to the goal's value.",
        "required": [
          "label",
          "source_type",
          "kind",
          "source_id",
          "trigger_id",
          "sent",
          "counted",
          "value",
          "asked",
          "note"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "source_type": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "The kind by its name in Communications, in the caller's language (\"Feed post\", \"Automated message\") — words to show; switch on `source_type`."
          },
          "source_id": {
            "type": "integer",
            "nullable": true
          },
          "trigger_id": {
            "type": "integer",
            "nullable": true
          },
          "sent": {
            "type": "integer"
          },
          "counted": {
            "type": "boolean"
          },
          "value": {
            "type": "integer",
            "nullable": true
          },
          "asked": {
            "type": "integer",
            "nullable": true,
            "description": "The people it asked to act (completion rate only)."
          },
          "note": {
            "type": "string",
            "description": "What its value means, or why it is not counted."
          }
        }
      },
      "CommsHubIssueBlock": {
        "type": "object",
        "required": [
          "id",
          "position",
          "block_type",
          "data"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "position": {
            "type": "integer"
          },
          "block_type": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "image_text",
              "two_columns",
              "quote",
              "stats",
              "divider",
              "hero",
              "callout",
              "button",
              "dynamic_recognitions",
              "dynamic_new_hires",
              "dynamic_top_news_posts",
              "dynamic_course_completions",
              "dynamic_open_shifts",
              "dynamic_pulse_results",
              "dynamic_recent_sites",
              "dynamic_recent_pages",
              "dynamic_upcoming_events",
              "dynamic_leader_rounds"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The block's content, keyed by its type — e.g. `text` → `{body}`,\n`image` → `{url, alt, href}`, `hero` → `{headline, subhead,\nimage_url, cta_label, cta_url}`, `callout` → `{body, tone}`,\n`button` → `{label, url, tone}`. A dynamic block's resolved data\ncarries `items` (rows) and, in a sent issue, `heading`,\n`description` and `metrics`.\n"
          }
        }
      },
      "CommsHubAudience": {
        "type": "object",
        "required": [
          "id",
          "name",
          "summary",
          "member_count_cache",
          "definition_type"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Store managers — West"
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "The audience's description."
          },
          "member_count_cache": {
            "type": "integer",
            "nullable": true,
            "description": "Cached member count; refreshed when the audience's rules change and nightly."
          },
          "definition_type": {
            "type": "string",
            "nullable": true,
            "description": "`composition` for an audience built from composed rules; otherwise the `type` of its legacy rule definition; null when it has neither."
          }
        }
      },
      "CommsHubApprovalRequestRow": {
        "type": "object",
        "description": "An approval request as a queue row. The names are the decision doors' (`approval` on `POST /approvals/{id}/approve`): `request_id` is the same id as `id`.",
        "required": [
          "id",
          "request_id",
          "status",
          "workflow",
          "requested_at",
          "current_step",
          "overdue",
          "due_soon",
          "can_act"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "request_id": {
            "type": "integer",
            "description": "The same id, under the name the decision doors use."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "withdrawn",
              "overridden"
            ]
          },
          "workflow": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "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",
                "nullable": true
              },
              "role": {
                "type": "string",
                "nullable": true,
                "description": "The role that decides this step, when the step names a role."
              },
              "label": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The step's deadline while the request waits; null when its workflow sets none, and once it is decided."
          },
          "overdue": {
            "type": "boolean"
          },
          "due_soon": {
            "type": "boolean",
            "description": "Inside the last quarter of the step's deadline window."
          },
          "can_act": {
            "type": "boolean",
            "description": "Whether the caller can decide it now — true on every queue row, unless the token lacks `write:broadcasts` (a read-only token decides nothing)."
          },
          "communication": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CommsHubApprovalCommunicationSummary"
              }
            ],
            "nullable": true,
            "description": "The message it is about; null when it has been deleted."
          }
        }
      },
      "CommsHubApprovalRequestDetail": {
        "type": "object",
        "description": "One approval request, as the web approval request page shows it.",
        "required": [
          "id",
          "request_id",
          "status",
          "workflow",
          "requested_at",
          "current_step",
          "overdue",
          "due_soon",
          "can_act",
          "can_reject",
          "can_withdraw",
          "steps",
          "decisions",
          "facts",
          "author_left",
          "web_url"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "request_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "withdrawn",
              "overridden"
            ]
          },
          "workflow": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "requested_by": {
            "$ref": "#/components/schemas/ApprovalUser"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "current_step": {
            "type": "object",
            "required": [
              "index",
              "total"
            ],
            "properties": {
              "index": {
                "type": "integer"
              },
              "total": {
                "type": "integer",
                "nullable": true
              },
              "role": {
                "type": "string",
                "nullable": true
              },
              "label": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "overdue": {
            "type": "boolean"
          },
          "due_soon": {
            "type": "boolean"
          },
          "can_act": {
            "type": "boolean",
            "description": "Whether the caller can approve the current step now — never on their own request (another approver decides it), unless they are a company admin, and never on a token without `write:broadcasts`."
          },
          "can_reject": {
            "type": "boolean",
            "description": "`can_act`, and the token carries rejecting's delete-level scope (`destructive:broadcasts`; an alert's, `destructive:safety_hub`)."
          },
          "can_withdraw": {
            "type": "boolean",
            "description": "Whether `POST /approvals/{id}/withdraw` takes it back for the caller now: it is pending, the caller asked for it or is a company admin (while Approvals is off, only the person who asked), and the token carries `write:broadcasts` and the delete-level scope."
          },
          "content_version": {
            "type": "integer",
            "nullable": true,
            "description": "The message's version as Unix seconds — send it back as `seen` on approve/reject, which then refuses (409 `content_changed`) if the message changed since it was read."
          },
          "steps": {
            "type": "array",
            "description": "Every step of the workflow, in order.",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "label": {
                  "type": "string",
                  "nullable": true
                },
                "approver": {
                  "type": "string",
                  "description": "Who decides it, as the workflow editor names it (\"Anyone with the manager role\")."
                }
              }
            }
          },
          "waiting_on": {
            "type": "string",
            "nullable": true,
            "description": "Who it waits on — never the person who asked; null once it waits on nobody (decided, withdrawn, or its message was sent or canceled)."
          },
          "author_left": {
            "type": "boolean",
            "description": "Its author has left the company, so approving it won't send it."
          },
          "superseded_by_id": {
            "type": "integer",
            "nullable": true,
            "description": "It was edited after it was sent for review, so its review started again: decide this request instead."
          },
          "decisions": {
            "type": "array",
            "description": "The audit trail, oldest first.",
            "items": {
              "type": "object",
              "properties": {
                "decision": {
                  "type": "string",
                  "description": "approve, reject, withdraw or sent_without_approval."
                },
                "step_index": {
                  "type": "integer",
                  "nullable": true
                },
                "role": {
                  "type": "string",
                  "nullable": true,
                  "description": "The role the decider acted as; null for a withdrawal or a send without approval."
                },
                "by": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "name": {
                      "type": "string",
                      "description": "A name, \"Deleted user\" once they are erased, or \"System\"."
                    }
                  }
                },
                "notes": {
                  "type": "string",
                  "nullable": true
                },
                "decided_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                }
              }
            }
          },
          "heading": {
            "type": "string",
            "nullable": true,
            "description": "What the message section is called in the request's tense — \"What you're approving\", \"What's under review\", \"What was approved\" …"
          },
          "consequence": {
            "type": "string",
            "nullable": true,
            "description": "While it waits: what approving does — who it is released to, and when. Null once it is decided."
          },
          "facts": {
            "type": "array",
            "description": "Who receives it, through which channels and when, and the rest the web page lists.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "value"
              ],
              "properties": {
                "label": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                },
                "hint": {
                  "type": "string",
                  "nullable": true
                },
                "tone": {
                  "type": "string",
                  "nullable": true,
                  "enum": [
                    "warning",
                    "danger",
                    null
                  ],
                  "description": "`danger` — a problem with a send still waiting (nobody would receive it); never once the request is decided or the message is out or canceled."
                }
              }
            }
          },
          "web_url": {
            "type": "string",
            "description": "The same request on the web."
          },
          "communication": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CommsHubApprovalCommunication"
              }
            ],
            "nullable": true,
            "description": "The message as its readers will see it; null when it has been deleted."
          }
        }
      },
      "CommsHubApprovalCommunicationSummary": {
        "type": "object",
        "required": [
          "type",
          "type_label",
          "source_type",
          "source_id",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "post",
              "broadcast",
              "newsletter",
              "emergency_alert",
              "survey",
              "contest",
              "triggered_message",
              "message"
            ],
            "description": "What kind of message it is — a stable key to switch on. It never follows a rename of the words: an automated message (a trigger's) is `triggered_message`. `message` is any other kind."
          },
          "type_label": {
            "type": "string",
            "example": "Feed post",
            "description": "The kind's name as the pages say it (\"Feed post\", \"Automated message\", \"Newsletter issue\"). Words to show, not to compare — they change with the product's copy; switch on `type`."
          },
          "source_type": {
            "type": "string",
            "example": "NewsFeed::Feed"
          },
          "source_id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "The message's own status (draft, scheduled, published, canceled …)."
          }
        }
      },
      "CommsHubApprovalCommunication": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CommsHubApprovalCommunicationSummary"
          },
          {
            "type": "object",
            "description": "The fields its kind adds. A post: the `GET /feeds/{id}` names; a broadcast: the `GET /broadcasts/{id}` names; a newsletter issue: its preview and where to read it.",
            "properties": {
              "kind": {
                "type": "string",
                "description": "Post and broadcast — post, announcement, must_read, break_through or in_context."
              },
              "kind_label": {
                "type": "string",
                "nullable": true,
                "description": "Post and broadcast — the reader's word for the kind (the company's own label when it chose one)."
              },
              "content_type": {
                "type": "string",
                "description": "Post — update, question or poll."
              },
              "priority": {
                "type": "string",
                "description": "Post — social, operational or must_read."
              },
              "must_read": {
                "type": "boolean"
              },
              "requires_acknowledgement": {
                "type": "boolean",
                "description": "Post — readers must acknowledge it."
              },
              "body": {
                "type": "string",
                "nullable": true,
                "description": "Post — the words as written, in Markdown (the pictures in them taken out when `pictures_shown` is false)."
              },
              "rendered_body": {
                "type": "string",
                "nullable": true,
                "description": "Post — the body as HTML, its pictures at storage URLs signed now (left out when `pictures_shown` is false)."
              },
              "inline_media_ids": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "description": "Post — the pictures the text shows; leave them out of the gallery. Empty when `pictures_shown` is false."
              },
              "topics": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              },
              "author": {
                "$ref": "#/components/schemas/ApprovalUser"
              },
              "scheduled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "published_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "pictures_shown": {
                "type": "boolean",
                "description": "Post — false for someone who may open the request but neither decides nor decided it and may not read the post: `media` and `inline_media_ids` are empty, `cover_image` null, and `body` and `rendered_body` have no pictures."
              },
              "pictures_withheld": {
                "type": "boolean",
                "description": "Post — true when `pictures_shown` is false and the post has pictures or files that were left out (a text-only post answers false). The web draws \"Its pictures and files load for the people deciding it.\" on it."
              },
              "media": {
                "type": "array",
                "description": "Post — its pictures and files, URLs signed for an hour.",
                "items": {
                  "$ref": "#/components/schemas/NewsFeedFeedMedia"
                }
              },
              "cover_image": {
                "type": "object",
                "nullable": true,
                "description": "Post — drawn at the top of the post and left out of the gallery.",
                "properties": {
                  "media_id": {
                    "type": "integer"
                  },
                  "url": {
                    "type": "string"
                  },
                  "alt": {
                    "type": "string",
                    "nullable": true
                  },
                  "width": {
                    "type": "integer",
                    "nullable": true
                  },
                  "height": {
                    "type": "integer",
                    "nullable": true
                  },
                  "link": {
                    "type": "string",
                    "nullable": true,
                    "description": "A \"Tell people\" post's cover opens the record it announces (the course, wiki or Sites page) — the same in-app path as the post's own link in `body`. Null on every other post."
                  }
                }
              },
              "poll": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NewsFeedPoll"
                  }
                ],
                "nullable": true,
                "description": "A poll — its choices; draw them read-only."
              },
              "description": {
                "type": "string",
                "nullable": true,
                "description": "Broadcast — its message, mentions as names."
              },
              "is_critical": {
                "type": "boolean"
              },
              "require_acknowledgment": {
                "type": "boolean"
              },
              "channels": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Broadcast — in_app, email, push, sms, voice."
              },
              "delivery_mode": {
                "type": "string",
                "description": "Broadcast — immediate, on_shift_only or next_shift_start."
              },
              "hero_image": {
                "type": "object",
                "nullable": true,
                "description": "Broadcast — its cover picture, signed for an hour.",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "alt": {
                    "type": "string"
                  },
                  "width": {
                    "type": "integer",
                    "nullable": true
                  },
                  "height": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              },
              "attachments": {
                "type": "array",
                "description": "Broadcast — its files, signed for an hour.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "content_type": {
                      "type": "string"
                    },
                    "byte_size": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              },
              "preview_text": {
                "type": "string",
                "nullable": true,
                "description": "Newsletter — the line under its subject."
              },
              "publication": {
                "type": "object",
                "description": "Newsletter — the publication it is an issue of.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "blocks_count": {
                "type": "integer"
              },
              "web_url": {
                "type": "string",
                "description": "Newsletter — the request's web page, which draws the issue whole."
              }
            }
          }
        ]
      },
      "DelegatedReview": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "post",
              "broadcast"
            ]
          },
          "message_kind": {
            "type": "string",
            "enum": [
              "post",
              "announcement",
              "must_read",
              "break_through",
              "in_context"
            ]
          },
          "message_kind_label": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "preview": {
            "type": "string",
            "description": "First 280 characters of the message as plain text."
          },
          "body": {
            "type": "string",
            "description": "The whole message as plain text."
          },
          "delegate": {
            "type": "object",
            "nullable": true,
            "description": "The delegate who wrote it; null when an app drafted it.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "written_by": {
            "type": "string",
            "nullable": true,
            "description": "The delegate's name, or the app that drafted it."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "send_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The send time the delegate picked; null to send on approval."
          },
          "approval_step": {
            "type": "boolean",
            "description": "Approving hands it to the company's reviewers instead of sending it."
          },
          "can_decide": {
            "type": "boolean",
            "description": "The token may approve or decline it (write:news_feed for a post, write:broadcasts for a broadcast)."
          },
          "seen": {
            "type": "integer",
            "description": "Send back as `seen` on approve."
          },
          "approve_path": {
            "type": "string"
          },
          "decline_path": {
            "type": "string"
          },
          "review_path": {
            "type": "string",
            "description": "The web review page."
          }
        }
      },
      "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"
              },
              "within_balance": {
                "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 apply region, audience, category enablement and `search`. They do NOT apply the active category (each row is an ALTERNATIVE to it), and they do not apply the active collection or `within_balance` either: this sheet is shared with `GET /company-store/catalog/categories`. The web and phone pills do count under the collection and \"Within my balance\"; the listing returns those counts in `filter_counts`. Use `has_items` to hide empty rows the way the web dropdown does.",
            "items": {
              "$ref": "#/components/schemas/CompanyStoreCategoryRow"
            }
          },
          "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. Counted like `categories`: not under `within_balance`.",
            "items": {
              "$ref": "#/components/schemas/CompanyStoreCollectionChip"
            }
          }
        }
      },
      "CompanyStoreCategoryRow": {
        "type": "object",
        "description": "One row of the category filter sheet.",
        "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"
          }
        }
      },
      "CompanyStoreCollectionChip": {
        "type": "object",
        "description": "One merchandised collection chip.",
        "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": "True for every row in this list when the caller's token holds `write:company_store` (the approve endpoint asks it; a read-only token gets `false`): 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. A session always passes the scope half."
          },
          "can_decline": {
            "type": "boolean",
            "description": "Whether the caller's token holds `destructive:company_store`, which the decline endpoint asks (a decline is a final act). Draw Decline off this flag and Approve off `can_approve`. A session always passes."
          }
        }
      },
      "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": "Every line of the order (\"Mug, Hoodie ×2\"), not only the representative one."
          },
          "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 — and a business admin's OWN hold while another business admin exists (nobody signs off their own redemption while someone else can). Render the controls off this flag; a decision POST on a `false` row answers `403 not_an_approver`. It is also `false` for a token without `write:company_store`, which the approve endpoint asks (a read-only token may list the queue but not decide it). The decision endpoints enforce the rule on write regardless, which is where a tier change under a stale list is caught."
          },
          "can_decline": {
            "type": "boolean",
            "description": "May THIS caller decline THIS row: they may decide it (the rule behind `can_approve`) AND the token holds `destructive:company_store`, which the decline endpoint asks (a decline is a final act). Draw Decline off this flag and Approve off `can_approve` — a write-only token may approve but its decline answers `403 insufficient_permissions`. A session always passes the scope half."
          },
          "order_url": {
            "type": "string",
            "description": "Absolute link to where THIS caller decides the hold — the manager approval queue when the order is in their queue there, else the order's admin page. (The employee's own order page refused every approver but a store admin.)"
          },
          "hold_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why it is held — the velocity rule that tripped, else \"Over the manager approval threshold\" / \"Over the admin approval threshold\"."
          }
        }
      },
      "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."
                  },
                  "can_decline": {
                    "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 ORD-20260731-1AOOOK — released for fulfillment."
          },
          "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",
                  "already_decided"
                ]
              },
              "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, already held under the balance, the monthly redemption cap and the team store budget)."
                    }
                  }
                }
              },
              "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,
                    "description": "Who the order is held for (\"admin\" for a velocity hold, which always goes to an admin). Only a full-points order is ever held — a points + card split goes to Stripe — so this is present only while the `points` option is available."
                  },
                  "velocity_hold_reason": {
                    "type": "string",
                    "nullable": true,
                    "description": "Present only while the `points` option is available, like `required_approver`."
                  },
                  "team_budget": {
                    "type": "object",
                    "nullable": true,
                    "description": "The team store budget that governs this employee's points checkout (null when none does); `over` is true when this cart's points exceed what is left, which the checkout will refuse.",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "remaining": {
                        "type": "integer"
                      },
                      "over": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "CompanyStoreCartCheckoutResponse": {
        "type": "object",
        "properties": {
          "placed": {
            "type": "boolean"
          },
          "replayed": {
            "type": "boolean",
            "description": "True when `idempotency_key` had already placed this order: nothing was written now and the cart was left as it is. When `placed`, the `message` says it was already placed — never celebrate it as new."
          },
          "replay_status": {
            "type": "string",
            "enum": [
              "placed",
              "cancelled",
              "declined"
            ],
            "description": "Present on a replay answered `placed: true` — what became of the order `idempotency_key` placed. `placed`: it stands (whatever its progress). `cancelled`: it was cancelled since (an abandoned card payment, the buyer, an admin). `declined`: its approver turned it down. Nothing was placed now in any of them, and nothing is owed; for `cancelled` and `declined` show the `message` (it says so and how to start again) and never a success screen.",
            "example": "cancelled"
          },
          "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",
              "withdrawn"
            ],
            "description": "`withdrawn` — the sender took it back while it was open; nobody decided it."
          },
          "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
          },
          "withdraw_url": {
            "type": "string",
            "nullable": true,
            "description": "Present only on the caller's OWN open request: POST it to take the request back (POST /company-store/order-requests/{id}/withdraw)."
          }
        }
      },
      "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."
          },
          "team_budget": {
            "type": "object",
            "nullable": true,
            "description": "The team store budget that governs this employee's points (null when none does, or no points path is offered); `over` is true when this redemption's points exceed what is left.",
            "properties": {
              "name": {
                "type": "string"
              },
              "remaining": {
                "type": "integer"
              },
              "over": {
                "type": "boolean"
              }
            }
          },
          "applies_to": {
            "type": "array",
            "description": "Which payment paths the hold applies to: `[\"points\"]` or `[]`. Only a full-points order is ever held — a pure-card checkout spends no points, and a points + card split is charged through Stripe and never held (a split whose points cover the whole price is placed as a points order).",
            "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. A replay of an order cancelled since is `true` too (nothing is owed on it): its `replay_status` says it was cancelled."
          },
          "replayed": {
            "type": "boolean",
            "description": "True when `idempotency_key` had already placed this order (a retry whose response the client never saw): nothing was written now. With `placed: true`, `message` says it was already placed — report it as an order already placed, never celebrate it as a new one. With `placed: false` it is that order's still-open payment: open `payment.checkout_url` as for a new one. False for a new order.",
            "example": false
          },
          "replay_status": {
            "type": "string",
            "enum": [
              "placed",
              "cancelled",
              "declined"
            ],
            "description": "Present on a replay answered `placed: true` — what became of the order `idempotency_key` placed. `placed`: it stands (whatever its progress). `cancelled`: it was cancelled since (an abandoned card payment, the buyer, an admin). `declined`: its approver turned it down. Nothing was placed now in any of them, and nothing is owed — which is all `placed: true` promises — so read `replay_status`, not `placed`, to tell them apart: for `cancelled` and `declined` show the `message` (it says so and how to start again), never a success screen.",
            "example": "placed"
          },
          "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. On a replay it says the order was already placed, or that it was cancelled (or not approved) since and how to order it again.",
            "example": "Order ORD-20260819-ABC123 placed."
          }
        }
      },
      "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\"; `cancelled` reads \"Cancelled / not approved\", since the bucket also holds redemptions an approver turned down.",
            "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`, \"Not approved\" for a `cancelled` order an approver turned down."
          },
          "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 that is still to come, so a gift card never promises a truck; once a shipped order is fulfilled it reads \"Fulfilled\", the order's own status (nothing records that it arrived).",
            "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_by": {
            "type": "object",
            "nullable": true,
            "description": "Set when a store admin placed the order for the employee (\"Redeem for employee\"), spending the employee's points. null for an order the employee placed themselves.",
            "properties": {
              "name": {
                "type": "string",
                "description": "The admin who placed it."
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "The reason the admin gave."
              }
            }
          },
          "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
              },
              "redemption_instructions": {
                "type": "string",
                "nullable": true,
                "description": "The provider's \"how to redeem\" text for the card, as plain text (links kept as \"label (url)\"). null when the provider sent none."
              },
              "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."
              }
            }
          },
          "not_approved": {
            "type": "object",
            "nullable": true,
            "description": "Present ONLY when an approver turned the redemption down (status `cancelled`, `status_label` \"Not approved\"): who decided, when and why — the same words the web and phone order pages show. null otherwise.",
            "properties": {
              "decided_by": {
                "type": "string",
                "nullable": true,
                "description": "In the viewer's words — \"your manager\", \"an admin\", or the approver group's name; \"their manager\" to an admin reading someone else's order."
              },
              "decided_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "The approver's own words."
              }
            }
          },
          "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",
                "enum": [
                  "requested",
                  "approved",
                  "rejected",
                  "withdrawn",
                  "replacement_sent"
                ],
                "description": "`withdrawn` — the buyer took it back while it was open; the order is free for a new return or report. `replacement_sent` — an approved exchange whose replacement the store has sent (see `replacement`)."
              },
              "return_type": {
                "type": "string",
                "enum": [
                  "refund",
                  "exchange"
                ]
              },
              "reason": {
                "type": "string"
              },
              "requested_at": {
                "type": "string",
                "format": "date-time"
              },
              "resolution_notes": {
                "type": "string",
                "nullable": true,
                "description": "The store admin's notes on the decision (why a return was not approved)."
              },
              "processed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "requested_variants": {
                "type": "object",
                "nullable": true,
                "additionalProperties": {
                  "type": "string"
                },
                "description": "An exchange's wanted variant as filed (e.g. {\"size\": \"XL\"}); null for a refund or an exchange that named none."
              },
              "requested_variant_label": {
                "type": "string",
                "nullable": true,
                "description": "The same in words, named by the item's own variant labels — \"Size: XL\"."
              },
              "replacement": {
                "type": "object",
                "nullable": true,
                "description": "An exchange's replacement once the store marked it sent; null until then (and for a refund).",
                "properties": {
                  "sent_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "carrier": {
                    "type": "string",
                    "nullable": true,
                    "description": "One of UPS, FedEx, USPS, DHL or Other; null when none was given."
                  },
                  "tracking_number": {
                    "type": "string",
                    "nullable": true
                  },
                  "tracking_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "The carrier's tracking page, when the carrier is one the store links."
                  }
                }
              },
              "withdraw_url": {
                "type": "string",
                "nullable": true,
                "description": "Present only while the caller's OWN request is open: POST it to take it back (POST /company-store/orders/{order_number}/withdraw-return)."
              }
            }
          },
          "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."
              },
              "return_url": {
                "type": "string",
                "nullable": true,
                "description": "Where `can_return` / `can_report_problem` is sent — POST /company-store/orders/{order_number}/request-return. Null when neither is offered."
              },
              "return_reasons": {
                "type": "array",
                "description": "The reasons that POST takes for this order, as the web form labels them: a return's physical reasons, or a problem report's digital ones. Empty when neither is offered.",
                "items": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    }
                  }
                }
              },
              "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
          },
          "refunded": {
            "type": "integer",
            "description": "Points cancelled, declined or refunded orders gave back in the window (positive). Neither earned nor spent.",
            "example": 1500
          },
          "expired": {
            "type": "integer",
            "description": "Points the tenant's expiry policy took in the window, as a POSITIVE magnitude (like `spent`). Show it whenever it is non-zero, or `transaction_count` counts rows no figure accounts for.",
            "example": 700
          },
          "transaction_count": {
            "type": "integer",
            "description": "Every row in the window, of any type — the sum of the rows behind `earned`, `spent`, `adjustments`, `refunded` and `expired` (and any future type).",
            "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",
          "refund"
        ],
        "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
          },
          "refund": {
            "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",
              "refund",
              null
            ]
          },
          "label": {
            "type": "string",
            "description": "Display label — one word per type on every surface, the web chip and row badge's (PointsTransaction.type_label), in display order: \"All\", \"Earned\", \"Redemption\", \"Refund\", \"Adjustment\", \"Expired\".",
            "example": "Earned"
          },
          "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 `adjustment` and `expiry` while their count is 0 and they are not selected. A client may ignore this and render all six."
          }
        }
      },
      "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",
              "refund"
            ]
          },
          "type_label": {
            "type": "string",
            "description": "The type's one word, matching the web badge and filter pill (PointsTransaction.type_label).",
            "example": "Earned"
          },
          "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. Points a recognition or a boost gave name the giver as they are now (\"Recognition from Dana Reyes\", \"Boost from Sam Ortiz\"; \"Deleted user\" once they are erased, \"Anonymous\" for an anonymous recognition). 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, with the words the web ledgers print for its kind (`label`) — the record itself is not resolved, which would cost a polymorphic load per row. 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
              },
              "label": {
                "type": "string",
                "description": "What the web ledgers print under the row for this kind of source, in product words (PointsTransaction#source_label) — never a module path.",
                "example": "Store order"
              }
            }
          },
          "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`."
              }
            }
          }
        }
      },
      "OperatingCountry": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "example": "DE"
          },
          "name": {
            "type": "string",
            "example": "Germany"
          },
          "currency_code": {
            "type": "string",
            "nullable": true,
            "example": "EUR"
          },
          "currency_symbol": {
            "type": "string",
            "nullable": true,
            "example": "€"
          },
          "calling_code": {
            "type": "string",
            "nullable": true,
            "example": "+49"
          },
          "flag_emoji": {
            "type": "string",
            "nullable": true
          },
          "works_council": {
            "type": "boolean",
            "description": "Whether this business marked the country as having a works council (Knowledge Layer X3). Read and confirmation reports then show counts, not names, for people in it."
          }
        }
      },
      "DemandSignalsSignal": {
        "type": "object",
        "description": "A demand signal (every column of the row).",
        "required": [
          "id",
          "business_id",
          "location_id",
          "signal_type",
          "signal_date",
          "signal_time",
          "impact_factor",
          "name",
          "description",
          "metadata",
          "active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "location_id": {
            "type": "integer"
          },
          "signal_type": {
            "type": "string",
            "enum": [
              "event",
              "weather",
              "holiday",
              "promotion",
              "sale",
              "seasonal",
              "external"
            ]
          },
          "signal_date": {
            "type": "string",
            "format": "date",
            "description": "The day the signal applies to."
          },
          "signal_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Optional moment within the day."
          },
          "impact_factor": {
            "type": "string",
            "description": "Multiplier on baseline demand, as a decimal string: \"1.0\" no effect, \"1.35\" a 35% lift, \"0.8\" a 20% drop.",
            "example": "1.35"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Required unless `signal_type` is \"weather\"."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Free-form JSON."
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Only `true` feeds forecasts; null (stored when an empty value was sent) is off."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DemandSignalsSignalWithLocation": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DemandSignalsSignal"
          },
          {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "$ref": "#/components/schemas/DemandSignalsLocation"
              }
            }
          }
        ]
      },
      "DemandSignalsSignalInput": {
        "type": "object",
        "description": "The fields a create or update may set. Any other key is ignored.",
        "properties": {
          "location_id": {
            "type": "integer",
            "description": "One of the business's locations (404 \"Location not found\" otherwise)."
          },
          "signal_type": {
            "type": "string",
            "enum": [
              "event",
              "weather",
              "holiday",
              "promotion",
              "sale",
              "seasonal",
              "external"
            ]
          },
          "signal_date": {
            "type": "string",
            "format": "date"
          },
          "signal_time": {
            "type": "string",
            "format": "date-time"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "impact_factor": {
            "type": "number",
            "description": "Greater than 0. Defaults to 1.0 on create."
          },
          "value": {
            "type": "number",
            "description": "Older name for `impact_factor`, used only when `impact_factor` is blank."
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Defaults to true. On update, anything that does not read as true switches the signal off (and asks `destructive:scheduling` when it was on)."
          },
          "metadata": {
            "type": "object",
            "description": "Free-form JSON; replaces the stored value."
          }
        }
      },
      "DemandSignalsLocation": {
        "type": "object",
        "description": "The signal's location — every column of the location row; the common ones are listed. Decimal columns (budgets, costs, latitude, longitude, weekly_contracted_hours) arrive as decimal strings.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "nullable": true
          },
          "zip": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "country_code": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "nullable": true
          },
          "parent_id": {
            "type": "integer",
            "nullable": true
          },
          "external_id": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "string",
            "nullable": true
          },
          "longitude": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DemandSignalsListMeta": {
        "type": "object",
        "required": [
          "current_page",
          "total_pages",
          "total_count"
        ],
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          }
        }
      },
      "DemandSignalsValidationMessages": {
        "type": "object",
        "description": "A refused save, as sentences a person can be shown.",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Impact factor must be greater than 0"
            ]
          }
        }
      },
      "DemandSignalsBulkItem": {
        "type": "object",
        "description": "One signal to create, checked by the job.",
        "required": [
          "location_id",
          "signal_type",
          "signal_date"
        ],
        "properties": {
          "location_id": {
            "type": "integer",
            "description": "One of the business's locations; otherwise the item fails (\"Location N not found in this business\")."
          },
          "signal_type": {
            "type": "string",
            "enum": [
              "event",
              "weather",
              "holiday",
              "promotion",
              "sale",
              "seasonal",
              "external"
            ]
          },
          "signal_date": {
            "type": "string",
            "format": "date"
          },
          "signal_time": {
            "type": "string",
            "format": "date-time"
          },
          "name": {
            "type": "string",
            "description": "Required unless `signal_type` is \"weather\". Part of the match with an existing signal."
          },
          "description": {
            "type": "string"
          },
          "impact_factor": {
            "type": "number",
            "description": "Greater than 0; 1.0 when omitted."
          },
          "metadata": {
            "type": "object"
          },
          "active": {
            "type": "boolean",
            "nullable": true
          }
        }
      },
      "DemandSignalsBulkOptions": {
        "type": "object",
        "properties": {
          "conflict_resolution": {
            "type": "string",
            "enum": [
              "skip",
              "overwrite",
              "fail"
            ],
            "default": "skip",
            "description": "What an item does when a signal with the same location, date, type and name already exists."
          },
          "validation_mode": {
            "type": "string",
            "enum": [
              "strict",
              "lenient"
            ],
            "default": "strict",
            "description": "strict stops the operation at the first failed item; lenient records it and continues."
          }
        }
      },
      "DemandSignalsBulkAccepted": {
        "type": "object",
        "required": [
          "bulk_operation",
          "job_id",
          "status_url"
        ],
        "properties": {
          "bulk_operation": {
            "$ref": "#/components/schemas/DemandSignalsBulkOperation"
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "status_url": {
            "type": "string",
            "description": "GET it for progress (GET /bulk_operations/status/{job_id}).",
            "example": "/api/v1/bulk_operations/status/0b6f1c1e-3c55-4f0b-9d0e-6a3c1f2f9a10"
          }
        }
      },
      "DemandSignalsBulkOperation": {
        "type": "object",
        "description": "The bulk operation row (every column).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null — this door does not record who started it."
          },
          "operation_type": {
            "type": "string",
            "example": "create"
          },
          "data_type": {
            "type": "string",
            "example": "demand_signals"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "metadata": {
            "type": "object",
            "description": "The submitted `items` and `options`, as the job will read them.",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "options": {
                "type": "object"
              }
            }
          },
          "total_items": {
            "type": "integer"
          },
          "processed_items": {
            "type": "integer"
          },
          "successful_items": {
            "type": "integer"
          },
          "failed_items": {
            "type": "integer"
          },
          "error_messages": {
            "type": "string",
            "nullable": true,
            "description": "Newline-separated item errors, each \"Item <index>: <reason>\" (index from 0), at most 50 recorded."
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "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"
          }
        }
      },
      "DigitalSignageScreen": {
        "type": "object",
        "description": "One signage screen (display), as `GET /digital-signage/screens` lists it.",
        "required": [
          "id",
          "name",
          "status",
          "online",
          "active",
          "last_seen_at",
          "location_id",
          "location_name",
          "is_sample_data",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 697
          },
          "name": {
            "type": "string",
            "example": "Break Room TV"
          },
          "status": {
            "type": "string",
            "enum": [
              "online",
              "offline",
              "inactive",
              "never_connected"
            ],
            "description": "`inactive` when switched off; otherwise `never_connected` (no heartbeat yet), `online` (heartbeat within 5 minutes) or `offline`."
          },
          "online": {
            "type": "boolean",
            "description": "Heartbeat within the last 5 minutes, regardless of `active`."
          },
          "active": {
            "type": "boolean",
            "description": "False when an admin switched the screen off."
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Last heartbeat (ISO 8601); null when the screen has never connected."
          },
          "awaiting_new_display_url": {
            "type": "boolean",
            "description": "True when the screen's display URL was replaced and no device has checked in on the new one yet — the device still has the old URL, which no longer plays. `online` can read true for a few minutes after the replacement."
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "The location the screen is bound to; null when unbound."
          },
          "location_name": {
            "type": "string",
            "nullable": true,
            "description": "Name of that location; null when unbound."
          },
          "is_sample_data": {
            "type": "boolean",
            "description": "True for a demo screen the \"Load sample data\" action registered — show it as sample data, never as real hardware (its `status` is not an outage). It is listed, filtered and counted like any other screen. A sample screen plays the business's real slides as well as the sample ones."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "display_url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute kiosk URL (`<host>/signage/display/<token>`). It embeds the screen's access token, the only credential the kiosk feed asks for — treat it as a secret. Emitted only to callers who may manage screens, which is every caller this list admits.",
            "example": "https://acme.workforce.mangoapps.com/signage/display/AbCdEf123456"
          }
        }
      },
      "DigitalSignageMediaAsset": {
        "type": "object",
        "description": "One media-library asset (slide), as `GET /digital-signage/media` lists it.",
        "required": [
          "id",
          "title",
          "media_kind",
          "status",
          "status_label",
          "caption",
          "media_url",
          "content_type",
          "location_id",
          "location_name",
          "active_from",
          "active_until",
          "within_schedule",
          "position",
          "is_sample_data",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "title": {
            "type": "string",
            "example": "Holiday hours"
          },
          "media_kind": {
            "type": "string",
            "enum": [
              "image",
              "video"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending",
              "approved",
              "rejected"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "Human label for `status`.",
            "enum": [
              "Draft",
              "Pending review",
              "Approved",
              "Rejected"
            ]
          },
          "caption": {
            "type": "string",
            "nullable": true
          },
          "media_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Absolute URL of the file (an Active Storage redirect); null when no file is attached."
          },
          "content_type": {
            "type": "string",
            "nullable": true,
            "description": "MIME type of the attached file; null when no file is attached.",
            "example": "image/png"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "The location the asset is shown at; null means all locations."
          },
          "location_name": {
            "type": "string",
            "nullable": true,
            "description": "Name of that location; null for an all-locations asset."
          },
          "active_from": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Start of the schedule window; null means no start limit."
          },
          "active_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "End of the schedule window; null means no end limit."
          },
          "within_schedule": {
            "type": "boolean",
            "description": "True when now falls inside the `active_from` / `active_until` window. Independent of approval."
          },
          "position": {
            "type": "integer",
            "description": "Sort position in the rotation (the list's primary order)."
          },
          "is_sample_data": {
            "type": "boolean",
            "description": "True for a demo slide the \"Load sample data\" action wrote — show it as sample data, not as content the business uploaded. It is listed and filtered like any other asset (`publishable=true` does not leave it out for being sample), but it plays only on screens whose `is_sample_data` is true."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Drive": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "name": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "DriveItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "drive_id": {
            "type": "integer",
            "minimum": 1
          },
          "parent_id": {
            "type": "integer",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "file",
              "folder"
            ]
          },
          "byte_size": {
            "type": "integer",
            "nullable": true
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "current_version_id": {
            "type": "integer",
            "nullable": true
          },
          "version_number": {
            "type": "integer",
            "nullable": true
          },
          "role": {
            "type": "string",
            "nullable": true
          },
          "owner_name": {
            "type": "string",
            "nullable": true
          },
          "updated_by_name": {
            "type": "string",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "scan_status": {
            "type": "string"
          },
          "can_preview": {
            "type": "boolean"
          },
          "can_download": {
            "type": "boolean"
          },
          "deleted_at": {
            "type": "string",
            "nullable": true
          },
          "restorable": {
            "type": "boolean"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical authenticated web route, not a file URL."
          }
        }
      },
      "DrivePagination": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "per_page": {
            "type": "integer",
            "minimum": 1
          },
          "total_pages": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          }
        }
      },
      "DriveConfig": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "settings": {
            "type": "object",
            "properties": {
              "max_upload_mb": {
                "type": "integer",
                "minimum": 1
              },
              "version_retention_count": {
                "type": "integer"
              },
              "external_links_enabled": {
                "type": "boolean"
              },
              "external_link_require_password": {
                "type": "boolean"
              },
              "external_link_max_days": {
                "type": "integer",
                "minimum": 1
              }
            }
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "write": {
                "type": "boolean"
              },
              "destructive": {
                "type": "boolean"
              },
              "chat": {
                "type": "boolean"
              },
              "edit": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "DrivePreview": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "pdf",
              "audio",
              "video",
              "web"
            ]
          },
          "text": {
            "type": "string"
          },
          "truncated": {
            "type": "boolean",
            "description": "Text exceeds the 262144-byte preview limit."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "content_type": {
            "type": "string"
          }
        }
      },
      "DriveVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "version_number": {
            "type": "integer",
            "minimum": 1
          },
          "byte_size": {
            "type": "integer",
            "nullable": true
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by_name": {
            "type": "string",
            "nullable": true
          },
          "change_summary": {
            "type": "string",
            "nullable": true
          },
          "scan_status": {
            "type": "string"
          },
          "can_restore": {
            "type": "boolean"
          },
          "asset_url": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "EmployeesCard": {
        "type": "object",
        "required": [
          "id",
          "name",
          "position",
          "manager",
          "department",
          "hire_date",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "position": {
            "type": "string",
            "nullable": true,
            "description": "The person's role type, else their effective role, else \"Employee\"; null when the People directory hides Job Function from the caller.",
            "example": "Employee"
          },
          "manager": {
            "type": "string",
            "nullable": true,
            "description": "The manager's name, only when that manager is an active member of this business; null otherwise, when the person has no manager, or when the People directory hides the manager from the caller."
          },
          "department": {
            "type": "object",
            "nullable": true,
            "description": "The person's organizational department in this business, as its id and name; null when they have none here, or when the People directory hides the department from the caller.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "example": "Bakery"
              }
            }
          },
          "hire_date": {
            "type": "string",
            "format": "date",
            "description": "The date the person's user account was created (`YYYY-MM-DD`) — not an HR hire date."
          },
          "status": {
            "type": "string",
            "enum": [
              "Active",
              "Inactive"
            ],
            "description": "Active while the person has an active membership in this business on an active account; Inactive once they have left it, whatever other organizations they still belong to."
          },
          "custom_fields": {
            "type": "array",
            "description": "The organization's own profile fields (People › Profile Fields) on this person that the caller may see — a coworker only those made visible to everyone. Absent from the short card of someone who has left.",
            "items": {
              "description": "One of the organization's own profile fields (People › Profile Fields)\non a person, as the caller may see it there: an administrator sees every\nfield; the person, their manager and coworkers see the fields the\nfield's visibility gives them. Section headings carry no answer and are\nnot listed.\n",
              "type": "object",
              "required": [
                "field_name",
                "label",
                "field_type",
                "category",
                "value",
                "display_value",
                "required",
                "editable"
              ],
              "properties": {
                "field_name": {
                  "type": "string",
                  "example": "tshirt_size"
                },
                "label": {
                  "type": "string",
                  "example": "T-shirt size"
                },
                "field_type": {
                  "type": "string",
                  "description": "One of text, textarea, number, email, phone, url, select, multiselect, radio, checkbox, date, datetime, time, file, currency, rating.",
                  "example": "select"
                },
                "category": {
                  "type": "string",
                  "description": "The profile section the field sits in.",
                  "example": "additional_info"
                },
                "value": {
                  "nullable": true,
                  "description": "The stored answer (a string, or an array for a multiselect); null when unanswered.",
                  "example": "L"
                },
                "display_value": {
                  "type": "string",
                  "nullable": true,
                  "description": "The answer as the profile shows it (a date formatted, a checkbox Yes/No, a rating as stars); null when unanswered.",
                  "example": "L"
                },
                "required": {
                  "type": "boolean"
                },
                "editable": {
                  "type": "boolean",
                  "description": "Whether the caller may change this answer."
                }
              }
            }
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "Only for a browser session or a token holding `read:users`, and only where the People directory shows email to the caller; left out otherwise."
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "description": "Only for a browser session or a token holding `read:users`, and only where the People directory shows phone to the caller; left out otherwise."
          }
        }
      },
      "EmployeesScheduleShift": {
        "type": "object",
        "required": [
          "id",
          "date",
          "start_time",
          "end_time",
          "location",
          "role",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The shift's start date in its location's time zone."
          },
          "start_time": {
            "type": "string",
            "description": "`HH:MM`, 24-hour, in the shift location's time zone.",
            "example": "09:00"
          },
          "end_time": {
            "type": "string",
            "nullable": true,
            "description": "`HH:MM`, 24-hour, in the shift location's time zone.",
            "example": "17:00"
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "The location's name; null for a virtual location or none."
          },
          "role": {
            "type": "string",
            "nullable": true,
            "description": "The role type of the shift's first role requirement, if any."
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "completed",
              "cancelled",
              "late_reported",
              "absence_reported"
            ]
          }
        }
      },
      "EmployeesScopeError": {
        "description": "The error envelope; a refusal that names its own `code` (the scope refusal, the colleague-schedule refusal) also repeats it at the top level.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "insufficient_permissions"
              }
            }
          }
        ]
      },
      "EsignatureRequestDetail": {
        "type": "object",
        "description": "One signature request as its detail view returns it. `signers`, `fields` and `audit_trail` list the request's signers in signing order, its fields and its history; a decline reason appears only to the people who answer for the request and to the signer who declined.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "voided"
          },
          "status_display": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "document_count": {
            "type": "integer"
          },
          "document_names": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "signing_progress": {},
          "can_edit": {
            "type": "boolean"
          },
          "can_send": {
            "type": "boolean"
          },
          "can_void": {
            "type": "boolean",
            "description": "Whether this caller may void the request — false for a token without `destructive:esignature`."
          },
          "can_resend": {
            "type": "boolean"
          },
          "signed_document_url": {
            "type": "string",
            "nullable": true,
            "description": "A download link for the signed document, valid for two hours; null until the request is completed, and null when `signed_document_sealed`."
          },
          "signed_document_sealed": {
            "type": "boolean"
          },
          "certificate_url": {
            "type": "string",
            "nullable": true
          },
          "scheduled": {
            "type": "boolean"
          },
          "scheduled_send_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "signers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EsignatureSignerDetail"
            }
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "cc_recipients": {
            "type": "array",
            "description": "People who receive the signed copy without signing. Each is emailed the signed PDF once the request completes (attached unless it prints protected personal numbers or is over 10 MB).",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "nullable": true
                },
                "email": {
                  "type": "string",
                  "format": "email"
                }
              }
            }
          },
          "cc_copies_sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the signed copies were sent to `cc_recipients`; null until then."
          },
          "audit_trail": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EsignatureAuditEvent"
            }
          }
        }
      },
      "EsignatureSignerSummary": {
        "type": "object",
        "description": "One signer on a request, in signing order position `signing_order`.",
        "required": [
          "id",
          "name",
          "email",
          "signs_in_app",
          "status",
          "status_label",
          "closed",
          "signing_order",
          "role"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "Null for a member who signs in the app with no email on file — their own policy, or a document sent to them in the app (e.g. a meal waiver); `signs_in_app` is then true."
          },
          "signs_in_app": {
            "type": "boolean",
            "description": "True for a member with no email on file, who signs while signed in to the app, never by an emailed link."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "sent",
              "viewed",
              "signed",
              "declined"
            ],
            "description": "The signer row's stored routing status. Ending a request (voided, declined, expired) does not change it — read `closed`."
          },
          "status_label": {
            "type": "string",
            "example": "Closed",
            "description": "The status as the request page shows it: `Closed` when `closed`, otherwise the status as a word (`Sent`, `Viewed`, `Signed`)."
          },
          "closed": {
            "type": "boolean",
            "description": "True when the request was voided, declined or expired before this signer signed or declined: nobody can sign it any more."
          },
          "signing_order": {
            "type": "integer",
            "minimum": 1
          },
          "role": {
            "type": "string",
            "nullable": true,
            "description": "The signer's role on the request (e.g. `signer`, `approver`, `witness`, `cc`)."
          }
        }
      },
      "EsignatureSignerDetail": {
        "description": "One signer as a request's detail view returns it.",
        "allOf": [
          {
            "$ref": "#/components/schemas/EsignatureSignerSummary"
          },
          {
            "type": "object",
            "required": [
              "is_turn",
              "internal"
            ],
            "properties": {
              "is_turn": {
                "type": "boolean",
                "description": "Whether this signer can sign now (everyone before them in the signing order has signed); always false once `closed`."
              },
              "internal": {
                "type": "boolean",
                "description": "True for a member of the organization."
              },
              "sent_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "viewed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "signed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "declined_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "decline_reason": {
                "type": "string",
                "nullable": true,
                "description": "The reason given on declining; null except to the people who answer for the request and to the signer who declined."
              }
            }
          }
        ]
      },
      "EsignatureAuditEvent": {
        "type": "object",
        "description": "One entry of a request's history, newest last.",
        "required": [
          "event",
          "label",
          "timestamp",
          "user_email",
          "details",
          "detail_pairs"
        ],
        "properties": {
          "event": {
            "type": "string",
            "example": "reminder_sent",
            "description": "The stored event key."
          },
          "label": {
            "type": "string",
            "example": "Reminder sent",
            "description": "The event as the CSV export and the signing certificate name it."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "user_email": {
            "type": "string",
            "nullable": true,
            "description": "Who did it; `system` for an event the platform originated, `unknown` when not recorded."
          },
          "details": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "The event's stored details, without internal row ids. A decline reason is withheld from anyone who may not see it, and the provider-reported IP address and browser from anyone outside the signing certificate's audience."
          },
          "detail_pairs": {
            "type": "array",
            "description": "The same details as the request page draws them — readable labels and values.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "value"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "example": "Channel"
                },
                "value": {
                  "type": "string",
                  "example": "Email and in-app notification"
                }
              }
            }
          }
        }
      },
      "EsignatureRequestSummary": {
        "type": "object",
        "description": "A signature request as the lists show it. `id` opens it at `GET /esignature/{id}`.",
        "required": [
          "id",
          "title",
          "status",
          "status_display",
          "created_at",
          "updated_at",
          "document_count",
          "signing_progress",
          "signer_count",
          "created_by",
          "signers"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "sent",
              "viewed",
              "partially_signed",
              "completed",
              "declined",
              "voided",
              "expired"
            ]
          },
          "status_display": {
            "type": "string",
            "description": "The status as a label (e.g. `Partially Signed`)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "document_count": {
            "type": "integer",
            "minimum": 0,
            "description": "How many documents the request carries."
          },
          "signing_progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Percentage of signers who have signed, rounded; 0 when there are no signers."
          },
          "signer_count": {
            "type": "integer",
            "minimum": 0
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "description": "Who created the request; null when the creator is unknown.",
            "required": [
              "id",
              "name",
              "email"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "signers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EsignatureSignerSummary"
            }
          }
        }
      },
      "EsignaturePendingRequest": {
        "description": "A request waiting for the caller's signature: the request summary plus the caller's own signer row on it.",
        "allOf": [
          {
            "$ref": "#/components/schemas/EsignatureRequestSummary"
          },
          {
            "type": "object",
            "required": [
              "my_signer_status",
              "my_signing_order",
              "is_my_turn"
            ],
            "properties": {
              "my_signer_status": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "pending",
                  "sent",
                  "viewed",
                  "signed",
                  "declined"
                ],
                "description": "The status of the caller's signer row."
              },
              "my_signing_order": {
                "type": "integer",
                "nullable": true,
                "description": "The caller's position in the signing order."
              },
              "is_my_turn": {
                "type": "boolean",
                "nullable": true,
                "description": "Whether the caller can sign now: every signer before them in the signing order has signed. False once the caller has signed or declined."
              }
            }
          }
        ]
      },
      "EsignatureTemplateSummary": {
        "type": "object",
        "description": "A template the business can send a signature request from.",
        "required": [
          "id",
          "name",
          "description",
          "category",
          "category_label",
          "system_template",
          "field_count",
          "signer_count",
          "signer_roles",
          "usage_count",
          "has_document",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "One of `hr`, `legal`, `sales`, `general`, `compliance`, `onboarding`, `other`; null or empty when the template has none."
          },
          "category_label": {
            "type": "string",
            "description": "The category as a label (e.g. `HR`, `Onboarding`); `General` when the category is null."
          },
          "system_template": {
            "type": "boolean",
            "description": "True for a platform system template, false for the business's own."
          },
          "field_count": {
            "type": "integer",
            "minimum": 0,
            "description": "How many fields the template defines."
          },
          "signer_count": {
            "type": "integer",
            "minimum": 0,
            "description": "How many signer roles the template defines."
          },
          "signer_roles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The signer role names, in order."
          },
          "usage_count": {
            "type": "integer",
            "minimum": 0,
            "description": "How many requests have been sent from the template."
          },
          "has_document": {
            "type": "boolean",
            "description": "Whether the template carries a reference PDF."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ExportFilesExportFile": {
        "type": "object",
        "description": "An export (every column of the row).",
        "required": [
          "id",
          "business_id",
          "user_id",
          "export_type",
          "format",
          "status",
          "job_id",
          "file_name",
          "content_type",
          "file_size",
          "download_url",
          "expires_at",
          "metadata",
          "started_at",
          "completed_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer",
            "nullable": true,
            "description": "Who started it; null for an export with no recorded requester (visible to admins only)."
          },
          "export_type": {
            "type": "string",
            "enum": [
              "shifts",
              "users",
              "attendance_records",
              "leave_requests",
              "shift_assignments",
              "assignments",
              "employees"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "csv",
              "json",
              "xlsx"
            ],
            "description": "File format. JSON is an array of objects keyed by the column names in snake_case; XLSX stops at 100,000 rows."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "file_name": {
            "type": "string",
            "nullable": true,
            "description": "Set once the file exists, e.g. \"shifts_20261010_101500.csv\"."
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "file_size": {
            "type": "integer",
            "nullable": true,
            "description": "Bytes."
          },
          "download_url": {
            "type": "string",
            "nullable": true,
            "description": "Once completed, the path of GET /export_files/{id}/download — call it for the signed file link. Null before.",
            "example": "/api/v1/export_files/41/download"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Seven days after completion; the file can no longer be downloaded after it."
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Bookkeeping: `shift_oversight` (a shifts export: the reach it was narrowed to), `record_count` (rows written, once completed) and `error` (a plain-language reason, once failed). An export started before 2026-10-10 may also carry the `filters` / `options` a typed route used to store; none was ever applied.",
            "properties": {
              "shift_oversight": {
                "type": "object",
                "properties": {
                  "covers_all": {
                    "type": "boolean"
                  },
                  "location_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "group_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "user_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              },
              "record_count": {
                "type": "integer"
              },
              "error": {
                "type": "string",
                "example": "The export timed out before it finished. Please request it again."
              }
            }
          },
          "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"
          }
        }
      },
      "ExportFilesTypedRequest": {
        "type": "object",
        "description": "Optional, in the JSON body or the query string. Rails also fills `format` from a path extension, so POST /export_files/shifts.json exports JSON whatever the body says. The typed routes take nothing else: the file holds every row of its type (a shifts export, the shifts the requester oversees), and a non-empty `filters` or `options` is refused with `422 unsupported_parameter`.",
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "csv",
              "json",
              "xlsx"
            ],
            "default": "csv",
            "description": "Blank means csv; any other value is refused (422 \"Format is not included in the list\")."
          }
        }
      },
      "ExportFilesForbidden": {
        "description": "The error envelope. A `forbidden` refusal also repeats its sentence as a top-level `message`; an `insufficient_permissions` one does not.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "message": {
                "type": "string",
                "example": "Exporting business-wide records requires manager access"
              }
            }
          }
        ]
      },
      "ExportFilesValidationMessages": {
        "type": "object",
        "description": "A refused save, as sentences a person can be shown.",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Export type is not included in the list"
            ]
          }
        }
      },
      "ForecastsForecast": {
        "type": "object",
        "description": "A per-day staffing forecast for one location (every column of the row).",
        "required": [
          "id",
          "business_id",
          "location_id",
          "forecast_date",
          "confidence",
          "hourly_predictions",
          "metadata",
          "source",
          "active",
          "is_sample_data",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "location_id": {
            "type": "integer"
          },
          "forecast_date": {
            "type": "string",
            "format": "date",
            "description": "The day the forecast covers. Unique per location."
          },
          "confidence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Confidence in the prediction, 0–100. Defaults to 0."
          },
          "hourly_predictions": {
            "type": "array",
            "nullable": true,
            "description": "The day's per-hour predictions, as stored.",
            "items": {
              "$ref": "#/components/schemas/ForecastsHourlyPrediction"
            }
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Free-form JSON."
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "Where the row came from — \"api\" (default) or \"derived\" (the baseline sweep). Not validated on write.",
            "example": "api"
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Only `true` is used in scheduling; null (stored when an empty value was sent) is off."
          },
          "is_sample_data": {
            "type": "boolean",
            "description": "True for a row the sample-data loader wrote."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ForecastsForecastWithLocation": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ForecastsForecast"
          },
          {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "$ref": "#/components/schemas/ForecastsLocation"
              }
            }
          }
        ]
      },
      "ForecastsForecastWithSignals": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ForecastsForecast"
          },
          {
            "type": "object",
            "required": [
              "demand_signals"
            ],
            "properties": {
              "demand_signals": {
                "type": "array",
                "description": "The live demand signals at this location on this date, highest `impact_factor` first.",
                "items": {
                  "$ref": "#/components/schemas/ForecastsDemandSignal"
                }
              }
            }
          }
        ]
      },
      "ForecastsForecastDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ForecastsForecastWithLocation"
          },
          {
            "type": "object",
            "required": [
              "demand_signals"
            ],
            "properties": {
              "demand_signals": {
                "type": "array",
                "description": "The live demand signals at this location on this date, highest `impact_factor` first.",
                "items": {
                  "$ref": "#/components/schemas/ForecastsDemandSignal"
                }
              }
            }
          }
        ]
      },
      "ForecastsForecastResponse": {
        "type": "object",
        "required": [
          "forecast"
        ],
        "properties": {
          "forecast": {
            "$ref": "#/components/schemas/ForecastsForecastDetail"
          },
          "demand_signal_errors": {
            "type": "array",
            "description": "Present only when a nested demand-signal write was refused; the forecast itself was saved. One entry per refused signal.",
            "items": {
              "type": "string"
            },
            "example": [
              "signal 2: Impact factor must be greater than 0",
              "signal 3: not found for this forecast"
            ]
          }
        }
      },
      "ForecastsHourlyPrediction": {
        "type": "object",
        "description": "One hour's prediction. Values are stored as sent. `hour` is required and must be 0–23; the staff counts may not be negative, and `min_staff_needed` may not exceed `max_staff_needed`.",
        "properties": {
          "hour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23
          },
          "predicted_volume": {
            "type": "number"
          },
          "min_staff_needed": {
            "type": "integer"
          },
          "optimal_staff_needed": {
            "type": "integer"
          },
          "max_staff_needed": {
            "type": "integer"
          }
        }
      },
      "ForecastsForecastInput": {
        "type": "object",
        "description": "The forecast fields a create or update may set. Any other key is ignored.",
        "properties": {
          "location_id": {
            "type": "integer",
            "description": "One of the business's locations (404 \"Location not found\" otherwise). Required on create; on update a blank value is not checked here and fails the save."
          },
          "forecast_date": {
            "type": "string",
            "format": "date"
          },
          "confidence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "source": {
            "type": "string",
            "description": "Defaults to \"api\"."
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Defaults to true. On update, anything that does not read as true switches the forecast off (and asks `destructive:scheduling` when it was on)."
          },
          "hourly_predictions": {
            "type": "array",
            "description": "Replaces the stored list. Only the five keys shown are kept on each element.",
            "items": {
              "$ref": "#/components/schemas/ForecastsHourlyPrediction"
            }
          },
          "metadata": {
            "type": "object",
            "description": "Free-form JSON; replaces the stored value."
          }
        }
      },
      "ForecastsNestedDemandSignalInput": {
        "type": "object",
        "description": "A demand signal written with a forecast; its location and date are the forecast's. A new signal needs `signal_type` and — unless the type is \"weather\" — `name`.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Update this signal (update only; ignored on create). It must be one of the forecast's live signals."
          },
          "signal_type": {
            "type": "string",
            "enum": [
              "event",
              "weather",
              "holiday",
              "promotion",
              "sale",
              "seasonal",
              "external"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "impact_factor": {
            "type": "number",
            "description": "Multiplier on baseline demand, greater than 0 (1.0 = no effect, the default for a new signal)."
          },
          "value": {
            "type": "number",
            "description": "Older name for `impact_factor`, used when `impact_factor` is blank."
          },
          "signal_time": {
            "type": "string",
            "format": "date-time"
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "A null is ignored. A present value that does not read as true switches the signal off."
          },
          "metadata": {
            "type": "object"
          }
        }
      },
      "ForecastsDemandSignal": {
        "type": "object",
        "description": "A demand signal as the forecast endpoints return it (without its location row).",
        "required": [
          "id",
          "business_id",
          "location_id",
          "signal_type",
          "signal_date",
          "signal_time",
          "impact_factor",
          "name",
          "description",
          "metadata",
          "active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "location_id": {
            "type": "integer"
          },
          "signal_type": {
            "type": "string",
            "enum": [
              "event",
              "weather",
              "holiday",
              "promotion",
              "sale",
              "seasonal",
              "external"
            ]
          },
          "signal_date": {
            "type": "string",
            "format": "date"
          },
          "signal_time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "impact_factor": {
            "type": "string",
            "description": "Multiplier on baseline demand, as a decimal string (e.g. \"1.35\").",
            "example": "1.35"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ForecastsLocation": {
        "type": "object",
        "description": "The forecast's location — every column of the location row; the common ones are listed. Decimal columns (budgets, costs, latitude, longitude, weekly_contracted_hours) arrive as decimal strings.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "nullable": true
          },
          "zip": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "country_code": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "nullable": true
          },
          "parent_id": {
            "type": "integer",
            "nullable": true
          },
          "external_id": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "string",
            "nullable": true
          },
          "longitude": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ForecastsListMeta": {
        "type": "object",
        "required": [
          "current_page",
          "total_pages",
          "total_count"
        ],
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer"
          }
        }
      },
      "ForecastsValidationMessages": {
        "type": "object",
        "description": "A refused save, as sentences a person can be shown.",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Forecast date has already been taken"
            ]
          }
        }
      },
      "ForecastsBulkItem": {
        "type": "object",
        "description": "One forecast to create, checked by the job. Unlike the single-record create, `hourly_predictions` and `metadata` are stored exactly as sent.",
        "required": [
          "location_id",
          "forecast_date"
        ],
        "properties": {
          "location_id": {
            "type": "integer",
            "description": "One of the business's locations; otherwise the item fails (\"Location N not found in this business\")."
          },
          "forecast_date": {
            "type": "string",
            "format": "date"
          },
          "hourly_predictions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ForecastsHourlyPrediction"
            }
          },
          "confidence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "source": {
            "type": "string"
          },
          "metadata": {
            "type": "object"
          },
          "active": {
            "type": "boolean",
            "nullable": true
          }
        }
      },
      "ForecastsBulkOptions": {
        "type": "object",
        "properties": {
          "conflict_resolution": {
            "type": "string",
            "enum": [
              "skip",
              "overwrite",
              "fail"
            ],
            "default": "skip",
            "description": "What an item does when a forecast already exists for its location and date."
          },
          "validation_mode": {
            "type": "string",
            "enum": [
              "strict",
              "lenient"
            ],
            "default": "strict",
            "description": "strict stops the operation at the first failed item; lenient records it and continues."
          }
        }
      },
      "ForecastsBulkAccepted": {
        "type": "object",
        "required": [
          "bulk_operation",
          "job_id",
          "status_url"
        ],
        "properties": {
          "bulk_operation": {
            "$ref": "#/components/schemas/ForecastsBulkOperation"
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "status_url": {
            "type": "string",
            "description": "GET it for progress (GET /bulk_operations/status/{job_id}).",
            "example": "/api/v1/bulk_operations/status/0b6f1c1e-3c55-4f0b-9d0e-6a3c1f2f9a10"
          }
        }
      },
      "ForecastsBulkOperation": {
        "type": "object",
        "description": "The bulk operation row (every column).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "user_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null — this door does not record who started it."
          },
          "operation_type": {
            "type": "string",
            "example": "create"
          },
          "data_type": {
            "type": "string",
            "example": "forecasts"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "metadata": {
            "type": "object",
            "description": "The submitted `items` and `options`, as the job will read them.",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "options": {
                "type": "object"
              }
            }
          },
          "total_items": {
            "type": "integer"
          },
          "processed_items": {
            "type": "integer"
          },
          "successful_items": {
            "type": "integer"
          },
          "failed_items": {
            "type": "integer"
          },
          "error_messages": {
            "type": "string",
            "nullable": true,
            "description": "Newline-separated item errors, each \"Item <index>: <reason>\" (index from 0), at most 50 recorded."
          },
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "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"
          }
        }
      },
      "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."
              }
            }
          }
        }
      },
      "FrontlineCampaignDetailLocation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "FrontlineCampaignDetailCourse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "openable": {
            "type": "boolean",
            "description": "Whether the caller can open the Training app (the page links the course only then)."
          }
        }
      },
      "FrontlineCampaignDetail": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "scheduled",
                  "active",
                  "closed"
                ]
              },
              "status_label": {
                "type": "string"
              },
              "priority": {
                "type": "string",
                "enum": [
                  "normal",
                  "important",
                  "critical"
                ]
              },
              "priority_label": {
                "type": "string"
              },
              "work_type": {
                "type": "string"
              },
              "work_type_label": {
                "type": "string",
                "description": "Floor-facing name (\"Post\" for an acknowledgement)."
              },
              "starts_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "ends_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "releases_on": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "The release date shown beside a scheduled badge."
              },
              "launched": {
                "type": "boolean"
              },
              "launched_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "closed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "viewer": {
            "type": "object",
            "properties": {
              "author_tier": {
                "type": "boolean"
              },
              "edit_mode": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "draft",
                  "live"
                ],
                "description": "\"live\" — editing a launched campaign asks whether the change is a correction or material. Null when the caller cannot edit right now (outside their span, or while a background launch is still writing the work)."
              },
              "can": {
                "type": "object",
                "properties": {
                  "view_coverage": {
                    "type": "boolean",
                    "description": "Launched, Coverage on, author tier."
                  },
                  "view_history": {
                    "type": "boolean"
                  },
                  "edit": {
                    "type": "boolean",
                    "description": "May open the edit form now. False while a background launch is still writing the work (release state `launching`)."
                  },
                  "edit_translations": {
                    "type": "boolean"
                  },
                  "clone": {
                    "type": "boolean"
                  },
                  "close": {
                    "type": "boolean"
                  },
                  "delete": {
                    "type": "boolean",
                    "description": "Drafts only, and not while a background launch is still writing the work."
                  },
                  "ask_question": {
                    "type": "boolean"
                  },
                  "withdraw_approval": {
                    "type": "boolean",
                    "description": "The page's Withdraw from approval control: not launched, a publication request is open, and the caller is in the submitter's tier."
                  }
                }
              }
            }
          },
          "release": {
            "type": "object",
            "nullable": true,
            "description": "The header's one primary action. Null once launched, for a campaign that is not draft or scheduled, or for a caller outside the author tier (a collaborator). While a background launch is fanning out the state is `launching` (disabled). An author whose span misses a target gets state `outside_span` (disabled, with `blocked_reason`); the reason offers submitting for approval only when this caller may submit.",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "launching",
                  "under_review",
                  "approval_workflow_missing",
                  "submit_for_approval",
                  "outside_span",
                  "no_targets",
                  "launch_refused",
                  "launch_with_reason",
                  "launch"
                ]
              },
              "action": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "submit_for_approval",
                  "launch"
                ]
              },
              "enabled": {
                "type": "boolean"
              },
              "blocked_reason": {
                "type": "string",
                "nullable": true,
                "description": "Why the control is disabled, in words to show beside it."
              },
              "requires_reason": {
                "type": "boolean",
                "description": "Launching over the load warning needs a reason recorded on the history."
              },
              "reason_min_length": {
                "type": "integer",
                "nullable": true
              },
              "locations_outside_span": {
                "type": "integer",
                "nullable": true
              },
              "refusal_code": {
                "type": "string",
                "nullable": true,
                "description": "On `launch_refused` only: which launcher rule refuses the launch (for example `no_inspection_template`, `no_single_assignee`, a stale workforce source). `blocked_reason` carries the words."
              }
            }
          },
          "approval": {
            "type": "object",
            "nullable": true,
            "description": "Pre-launch only.",
            "properties": {
              "required": {
                "type": "boolean",
                "description": "The tenant requires publication approval."
              },
              "state": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "approved",
                  "under_review",
                  "workflow_missing",
                  "changes_requested",
                  "approval_stale",
                  "required"
                ],
                "description": "`changes_requested` — the latest request was sent back; `approval_stale` — approved, then edited, so it needs approval again."
              },
              "sent_back": {
                "type": "object",
                "nullable": true,
                "description": "Present on `changes_requested`: who sent the latest request back and their comment.",
                "properties": {
                  "by": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  "comment": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "stale_reason": {
                "type": "string",
                "nullable": true,
                "description": "Present on `approval_stale`: the sentence the page shows. It asks the caller to submit again only when this caller holds the Submit button (release state `submit_for_approval`)."
              }
            }
          },
          "coverage": {
            "type": "object",
            "nullable": true,
            "description": "Launched only; span-bounded.",
            "properties": {
              "unit": {
                "type": "string",
                "enum": [
                  "people",
                  "locations"
                ],
                "description": "What the headline tile counts. The other tiles always count locations."
              },
              "total": {
                "type": "integer"
              },
              "done": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "in_progress": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "not_started": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "missed": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "blocked": {
                "type": "integer"
              },
              "unassigned": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "percent": {
                "type": "integer",
                "nullable": true,
                "description": "Null while `people_withheld` is present."
              },
              "people": {
                "type": "object",
                "nullable": true,
                "description": "Posts (acknowledgements) only; null while `people_withheld` is present.",
                "properties": {
                  "expected": {
                    "type": "integer"
                  },
                  "confirmed": {
                    "type": "integer"
                  }
                }
              },
              "people_withheld": {
                "type": "object",
                "description": "Knowledge Layer X3 (#4546) — present only on a post a works council covers whose caller's whole span holds fewer people than the company's smallest report group. `unit` stays `people`, `people` and the status counts are null; show `reason` in the headline tile instead.",
                "properties": {
                  "reason": {
                    "type": "string",
                    "example": "Fewer than 5 people — counted in the level above."
                  }
                }
              }
            }
          },
          "launch_gaps": {
            "type": "object",
            "nullable": true,
            "description": "The launch-time snapshot of what the launch left out; null when nothing was.",
            "properties": {
              "skipped_count": {
                "type": "integer",
                "description": "Targeted locations that received nothing — the caller's own locations when the launch recorded every location by name, else the campaign-wide count (`skipped_campaign_wide`)."
              },
              "skipped_campaign_wide": {
                "type": "boolean",
                "description": "The launch's stored list was cut short, so `skipped_count` is the campaign's and may include locations outside the caller's area."
              },
              "skipped": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "reason_label": {
                      "type": "string"
                    }
                  }
                }
              },
              "skipped_overflow": {
                "type": "integer",
                "description": "The \"…and N others\" after the named list; 0 when nothing is listed or the count is campaign-wide."
              },
              "unstaffed_count": {
                "type": "integer",
                "description": "Targeted locations with nobody assigned — the same span rule as `skipped_count`."
              },
              "unstaffed_campaign_wide": {
                "type": "boolean"
              },
              "unstaffed": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetailLocation"
                }
              },
              "unstaffed_overflow": {
                "type": "integer",
                "description": "The same rule as `skipped_overflow`."
              },
              "recorded_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "required_training": {
            "type": "object",
            "nullable": true,
            "description": "Launched and training-gated only.",
            "properties": {
              "courses": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetailCourse"
                }
              },
              "holders_total": {
                "type": "integer"
              },
              "holders_complete": {
                "type": "integer"
              },
              "pending_count": {
                "type": "integer"
              },
              "pending_names": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "At most five."
              },
              "pending_overflow": {
                "type": "integer"
              },
              "all_complete": {
                "type": "boolean"
              }
            }
          },
          "readiness": {
            "type": "object",
            "nullable": true,
            "properties": {
              "ready": {
                "type": "boolean"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              },
              "availability": {
                "type": "object",
                "properties": {
                  "applicable": {
                    "type": "boolean"
                  },
                  "checked": {
                    "type": "boolean",
                    "description": "False above the inline size bound — then say who-can-take-it was not checked."
                  },
                  "failed": {
                    "type": "boolean"
                  }
                }
              }
            }
          },
          "writing_check": {
            "type": "object",
            "nullable": true,
            "description": "Pre-launch, AI on, lifecycle tier only.",
            "properties": {
              "measured_against": {
                "type": "string",
                "enum": [
                  "content_standards",
                  "clarity_baseline"
                ]
              },
              "result": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "verdict": {
                    "type": "string"
                  },
                  "reads_well": {
                    "type": "boolean"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  },
                  "dead_links": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "targets": {
            "type": "object",
            "nullable": true,
            "description": "The reviewable roster (pre-launch). Names are span-bounded and capped; counts are true totals.",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "locations",
                  "audience",
                  "everywhere"
                ]
              },
              "mode_label": {
                "type": "string"
              },
              "count": {
                "type": "integer"
              },
              "not_targeted_count": {
                "type": "integer",
                "nullable": true,
                "description": "Audience mode only."
              },
              "concerns": {
                "type": "boolean"
              },
              "locations": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetailLocation"
                }
              },
              "locations_overflow": {
                "type": "integer"
              },
              "dropped_count": {
                "type": "integer"
              },
              "dropped": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "explanation": {
                      "type": "string"
                    }
                  }
                }
              },
              "dropped_overflow": {
                "type": "integer"
              },
              "unstaffed_count": {
                "type": "integer"
              },
              "unstaffed": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetailLocation"
                }
              },
              "unstaffed_overflow": {
                "type": "integer"
              }
            }
          },
          "audience_load": {
            "type": "object",
            "nullable": true,
            "description": "The overload gauge (pre-launch).",
            "properties": {
              "tier": {
                "type": "string",
                "enum": [
                  "none_targeted",
                  "over_capacity",
                  "approaching_capacity",
                  "has_room"
                ]
              },
              "overloaded": {
                "type": "boolean"
              },
              "window_days": {
                "type": "integer"
              },
              "headcount": {
                "type": "integer"
              },
              "per_person": {
                "type": "number"
              },
              "total_open_items": {
                "type": "integer"
              },
              "capacity": {
                "type": "number"
              },
              "counts": {
                "type": "object",
                "properties": {
                  "campaign_items": {
                    "type": "integer"
                  },
                  "tasks": {
                    "type": "integer"
                  },
                  "must_reads": {
                    "type": "integer"
                  },
                  "training_due": {
                    "type": "integer"
                  }
                }
              },
              "scheduled_minutes": {
                "type": "integer",
                "nullable": true,
                "description": "Null (not 0) when Scheduling can't answer."
              },
              "estimated_minutes_total": {
                "type": "integer"
              },
              "unestimated_items": {
                "type": "integer"
              },
              "degraded_sources": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "recommendation": {
                "type": "string"
              },
              "computed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "brief": {
            "type": "object",
            "properties": {
              "instructions_html": {
                "type": "string",
                "nullable": true,
                "description": "Sanitized rich text."
              },
              "instructions_text": {
                "type": "string",
                "nullable": true
              },
              "checklist": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "documents": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "byte_size": {
                      "type": "integer"
                    },
                    "content_type": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "description": "Signed inline URL, valid for one hour."
                    }
                  }
                }
              }
            }
          },
          "setup": {
            "type": "object",
            "description": "Every row of \"How this campaign is set up\".",
            "properties": {
              "work": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "template_label": {
                    "type": "string",
                    "nullable": true
                  },
                  "inspection": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "checks": {
                        "type": "integer"
                      },
                      "critical": {
                        "type": "integer"
                      },
                      "needing_photo": {
                        "type": "integer"
                      },
                      "pass_threshold": {
                        "type": "number",
                        "nullable": true
                      },
                      "require_signature": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              },
              "assignment": {
                "type": "object",
                "properties": {
                  "strategy": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "timing": {
                    "type": "string",
                    "enum": [
                      "each_shift",
                      "on_pickup",
                      "at_launch"
                    ]
                  },
                  "timing_label": {
                    "type": "string"
                  }
                }
              },
              "targeting_label": {
                "type": "string"
              },
              "owning_department": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "estimated_duration_minutes": {
                "type": "integer",
                "nullable": true
              },
              "priority": {
                "type": "string"
              },
              "priority_label": {
                "type": "string"
              },
              "recurrence": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "pattern": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "until": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "on_new_occurrence": {
                    "type": "string",
                    "nullable": true
                  },
                  "every_shift": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "per": {
                        "type": "string",
                        "enum": [
                          "assignee",
                          "location"
                        ]
                      },
                      "offset_minutes": {
                        "type": "integer",
                        "description": "Negative = before the anchor."
                      },
                      "anchor": {
                        "type": "string",
                        "enum": [
                          "start",
                          "end"
                        ]
                      }
                    }
                  }
                }
              },
              "occurrence_of": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "campaign_id": {
                    "type": "integer"
                  },
                  "occurrence_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  }
                }
              },
              "eligibility_window": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "label": {
                    "type": "string"
                  }
                }
              },
              "on_miss": {
                "type": "object",
                "properties": {
                  "raises_corrective_action": {
                    "type": "boolean"
                  }
                }
              },
              "on_unfulfilled": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "close_behavior": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "proof": {
                "type": "object",
                "properties": {
                  "captured_in_inspection": {
                    "type": "boolean"
                  },
                  "photo": {
                    "type": "boolean"
                  },
                  "signature": {
                    "type": "boolean"
                  }
                }
              },
              "review": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "workflow",
                      "workflow_unavailable",
                      "single_reviewer",
                      "none"
                    ]
                  },
                  "workflow": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "stages": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "certifications": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "skill_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "name": {
                      "type": "string",
                      "nullable": true
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "minimum_proficiency": {
                      "type": "integer"
                    },
                    "require_verified": {
                      "type": "boolean"
                    },
                    "jurisdiction": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "available": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              },
              "required_response": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "form",
                      "survey"
                    ]
                  },
                  "source_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "name": {
                    "type": "string",
                    "nullable": true
                  },
                  "available": {
                    "type": "boolean"
                  },
                  "per_work_item": {
                    "type": "boolean"
                  }
                }
              },
              "required_training": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FrontlineCampaignDetailCourse"
                }
              }
            }
          },
          "shift_attention_reason": {
            "type": "string",
            "nullable": true
          },
          "upcoming_shift_work": {
            "type": "object",
            "nullable": true,
            "description": "Every-shift schedules only — the next seven days at the caller's locations.",
            "properties": {
              "error": {
                "type": "string",
                "nullable": true
              },
              "shifts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "shift_id": {
                      "type": "integer"
                    },
                    "location": {
                      "$ref": "#/components/schemas/FrontlineCampaignDetailLocation"
                    },
                    "time_zone": {
                      "type": "string"
                    },
                    "starts_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "due_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "work_items": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "questions": {
            "type": "object",
            "properties": {
              "post_count": {
                "type": "integer",
                "description": "Questions and replies."
              },
              "question_count": {
                "type": "integer"
              },
              "accepting_questions": {
                "type": "boolean"
              },
              "can_post": {
                "type": "boolean"
              },
              "max_body_length": {
                "type": "integer"
              },
              "threads": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "body": {
                      "type": "string"
                    },
                    "author": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "photo_url": {
                          "type": "string",
                          "description": "The author's avatar — uploaded profile photo, else a generated initials avatar. Always absolute."
                        }
                      }
                    },
                    "is_campaign_owner": {
                      "type": "boolean"
                    },
                    "parent_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "edited": {
                      "type": "boolean"
                    },
                    "replies": {
                      "type": "array",
                      "description": "The replies, oldest first — each the same shape as a question (author carries photo_url too), without its own replies.",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              },
              "meta": {
                "type": "object",
                "properties": {
                  "showing": {
                    "type": "integer"
                  },
                  "total": {
                    "type": "integer"
                  },
                  "truncated": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      },
      "FrontlineExecutionCampaignsResult": {
        "type": "object",
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FrontlineExecutionCampaignSummary"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/FrontlineExecutionCampaignsMeta"
          }
        },
        "required": [
          "campaigns",
          "meta"
        ]
      },
      "FrontlineExecutionCampaignSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "One of draft/scheduled/active/closed."
          },
          "priority": {
            "type": "string"
          },
          "assignment_strategy": {
            "type": "string"
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "coverage": {
            "$ref": "#/components/schemas/FrontlineExecutionCampaignCoverage"
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "coverage"
        ]
      },
      "FrontlineExecutionCampaignCoverage": {
        "type": "object",
        "description": "This campaign's obligations rolled up by status, bounded to the caller's location span.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Every obligation for this campaign in the caller's span."
          },
          "done": {
            "type": "integer",
            "nullable": true,
            "description": "Completed obligations."
          },
          "in_progress": {
            "type": "integer",
            "nullable": true,
            "description": "Obligations actively being worked — claimed, in progress, submitted for review, or reopened."
          },
          "awaiting_review": {
            "type": "integer",
            "nullable": true,
            "description": "The subset of `in_progress` submitted and waiting on a reviewer. Additive — `in_progress` still counts it. Same value as the Coverage endpoint's `awaiting_review` for this campaign."
          },
          "not_started": {
            "type": "integer",
            "nullable": true,
            "description": "Obligations still open (unstarted), assigned or not."
          },
          "missed": {
            "type": "integer",
            "nullable": true,
            "description": "Obligations whose window closed with no completion."
          },
          "blocked": {
            "type": "integer",
            "description": "Obligations a worker has flagged as blocked (any status). Not a status bucket, so it overlaps the others. Same value as the Coverage endpoint's `blocked` for this campaign."
          },
          "unassigned": {
            "type": "integer",
            "nullable": true,
            "description": "The subset of `not_started` with no holder — open work still in the claim pool."
          },
          "percent": {
            "type": "integer",
            "nullable": true,
            "description": "How far along the campaign is, as a 0–100 whole number, in the `unit` it names. For a post (`unit: people`) it is the share of its people who confirmed it at the caller's stores (`people.confirmed` ÷ `people.expected`; 100 when nobody is expected) — the figure its own Coverage page heads with, not done ÷ total (a post's store is done only once everyone there has confirmed). For every other campaign it is done ÷ total (0 when total is 0)."
          },
          "unit": {
            "type": "string",
            "enum": [
              "people",
              "locations"
            ],
            "description": "What `percent` counts — `people` for a post with work in the caller's span, else `locations`. The status counts always count obligations (one per location). Additive."
          },
          "people": {
            "type": "object",
            "nullable": true,
            "description": "A post's \"N of M people confirmed\" pair at the caller's stores, the numbers `percent` is drawn from. Null for every other work type, for a post with no work in the span, and while `people_withheld` is present. Additive.",
            "properties": {
              "expected": {
                "type": "integer",
                "description": "The people the post asks to confirm it at these stores."
              },
              "confirmed": {
                "type": "integer",
                "description": "Those of them who have confirmed it."
              }
            }
          },
          "people_withheld": {
            "type": "object",
            "description": "Knowledge Layer X3 (#4546) — present only on a post a works council covers whose caller's whole span holds fewer people than the company's smallest report group. `done`, `in_progress`, `awaiting_review`, `not_started`, `missed`, `unassigned`, `percent` and `people` are then null; show `reason` instead.",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Fewer than 5 people — counted in the level above."
              }
            }
          }
        },
        "required": [
          "total",
          "done",
          "in_progress",
          "awaiting_review",
          "not_started",
          "missed",
          "blocked",
          "unassigned",
          "percent",
          "unit",
          "people"
        ]
      },
      "FrontlineExecutionCampaignsMeta": {
        "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"
          },
          "status_filter_ignored": {
            "type": "boolean",
            "description": "Present and true only when a `status` param was sent but not applied (non-scalar or outside the allowed set)."
          },
          "status_filter_note": {
            "type": "string",
            "description": "Human-readable explanation, present alongside status_filter_ignored."
          }
        },
        "required": [
          "current_page"
        ]
      },
      "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."
              }
            }
          }
        }
      },
      "CampaignCoverage": {
        "type": "object",
        "description": "One campaign's coverage, span-bounded to the caller.",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "The campaign's lifecycle status",
                "e.g. active/closed.": null
              },
              "priority": {
                "type": "string",
                "enum": [
                  "normal",
                  "important",
                  "critical"
                ],
                "description": "The campaign's urgency band."
              },
              "work_type": {
                "type": "string",
                "enum": [
                  "task",
                  "inspection",
                  "acknowledgement"
                ],
                "description": "What KIND of work this campaign is."
              },
              "work_type_label": {
                "type": "string",
                "description": "The floor-facing label for `work_type` — \"Task\", \"Inspection\" or \"Post\" (an acknowledgement)."
              }
            },
            "required": [
              "id",
              "name",
              "status",
              "priority",
              "work_type",
              "work_type_label"
            ]
          },
          "summary": {
            "type": "object",
            "description": "Counted in SQL over the WHOLE campaign inside the caller's span — the Coverage summary card the campaign-detail screen shows above its location breakdown. While `people_withheld` is present the status counts and `percent` are null (a post's store is done only when everyone there confirmed, so they would be those few people's own answers) and `people_withheld` is true.",
            "properties": {
              "total": {
                "type": "integer"
              },
              "done": {
                "type": "integer",
                "nullable": true
              },
              "in_progress": {
                "type": "integer",
                "nullable": true,
                "description": "claimed / in_progress / submitted / reopened"
              },
              "not_started": {
                "type": "integer",
                "nullable": true,
                "description": "open obligations"
              },
              "missed": {
                "type": "integer",
                "nullable": true
              },
              "blocked": {
                "type": "integer",
                "description": "obligations flagged blocked (an annotation on top of their status)"
              },
              "unassigned": {
                "type": "integer",
                "nullable": true,
                "description": "open obligations with no holder — still in the claim pool; null while `people_withheld` is present"
              },
              "percent": {
                "type": "integer",
                "nullable": true,
                "description": "done ÷ total, 0–100 (0 when total is 0)"
              },
              "people_withheld": {
                "type": "boolean",
                "description": "Present (true) only while the top-level `people_withheld` is."
              }
            },
            "required": [
              "total",
              "done",
              "in_progress",
              "not_started",
              "missed",
              "blocked",
              "unassigned",
              "percent"
            ]
          },
          "hierarchy": {
            "type": "object",
            "description": "The `?node=` tree drill — the same rollup the web",
            "properties": {
              "location": {
                "type": "object",
                "nullable": true,
                "description": "The location selected with `?node=` (the drill's parent). Null without a node, and null for a node outside the caller's span or another tenant's id. Only `hierarchy` is bounded to it — `summary` and `by_location` still describe the caller's whole span.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "breadcrumb": {
                "type": "array",
                "description": "The span-clipped path from the caller's span top down to `node`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              },
              "leaf_level": {
                "type": "boolean",
                "description": "True when `nodes` are individual stores (the tree bottomed out)."
              },
              "nodes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CampaignCoverageRollupNode"
                }
              }
            }
          },
          "by_location": {
            "type": "array",
            "description": "A flat, worst-first list of the locations the campaign touches in the caller's span, sliced to `limit`.",
            "items": {
              "$ref": "#/components/schemas/CampaignCoverageLocationRow"
            }
          },
          "locations_total": {
            "type": "integer",
            "description": "Locations in scope BEFORE the slice."
          },
          "locations_shown": {
            "type": "integer",
            "description": "Rows actually returned in `by_location`."
          },
          "has_more_locations": {
            "type": "boolean",
            "description": "Whether `locations_total` exceeds `locations_shown` (raise `limit` to see more)."
          },
          "names_hidden": {
            "type": "object",
            "description": "Knowledge Layer X3 — present only on an acknowledgement campaign whose audience a works council covers (General Settings › Countries). Areas and locations with fewer people than the company's smallest report group are then folded into one trailing \"Other — N …\" row (`other: true`) in `hierarchy.nodes` and `by_location`, exactly as the web page does.",
            "properties": {
              "count": {
                "type": "integer",
                "nullable": true,
                "description": "People the report leaves unnamed — null while they are fewer than the company's smallest report group (see `fewer_than`)."
              },
              "fewer_than": {
                "type": "integer",
                "description": "Present only when `count` is null: the company's smallest report group. The hidden people are \"a few\" — fewer than this."
              },
              "reason": {
                "type": "string",
                "example": "Names are hidden for people in Germany (works council)."
              }
            }
          },
          "people_withheld": {
            "type": "object",
            "description": "Knowledge Layer X3 (#4546) — present only when the caller's whole span holds fewer people than the company's smallest report group on a post a works council covers. No figure counts people then: `by_location` is empty, `hierarchy.nodes` holds one `withheld` node, and the summary's status counts are null. Show `reason` instead.",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Fewer than 5 people — counted in the level above."
              }
            }
          }
        },
        "required": [
          "campaign",
          "summary",
          "hierarchy",
          "by_location",
          "locations_total",
          "locations_shown",
          "has_more_locations"
        ]
      },
      "CampaignCoverageLocationRow": {
        "type": "object",
        "description": "One location's coverage on this campaign.",
        "properties": {
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null for a row whose location has since been deleted, and for the folded `other` row."
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "The location's name (null for a deleted location). On the folded `other` row, its label — \"Other — 3 locations\"."
          },
          "other": {
            "type": "boolean",
            "description": "Present (true) only on the one folded row a works council asks for (see `names_hidden`): the small locations counted together."
          },
          "location_count": {
            "type": "integer",
            "description": "On the `other` row only — how many locations it folds."
          },
          "done": {
            "type": "integer",
            "description": "Completed obligations at this location."
          },
          "total": {
            "type": "integer",
            "description": "Total obligations at this location."
          },
          "percent": {
            "type": "integer",
            "description": "done ÷ total for task work; confirmed ÷ expected people for an acknowledgement campaign. 0–100."
          },
          "status": {
            "type": "string",
            "enum": [
              "on_track",
              "at_risk",
              "behind"
            ],
            "description": "Coverage-health band derived from `percent` — the SAME judgement the web coverage page announces per location. on_track ≥ 80%, at_risk 40–79%, behind < 40%."
          },
          "accountable": {
            "type": "string",
            "nullable": true,
            "description": "The person on the hook for this location — the leading obligation's assignee, else the location's accountable owner. Null when neither resolves."
          }
        },
        "required": [
          "location_id",
          "location",
          "done",
          "total",
          "percent",
          "status",
          "accountable"
        ]
      },
      "CampaignCoverageRollupNode": {
        "type": "object",
        "description": "One node of the `?node=` hierarchy drill, its subtree rolled up.",
        "properties": {
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "Null on the folded `other` node."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "The location's name; on the `other` node its label, e.g. \"Other — 2 areas\"."
          },
          "other": {
            "type": "boolean",
            "description": "Present (true) only on the one trailing node a works council folds small areas into (see `names_hidden`)."
          },
          "withheld": {
            "type": "boolean",
            "description": "Present (true) only on the one node that stands for a whole level below the company's smallest report group: `name` is the sentence to show and every figure is null."
          },
          "location_count": {
            "type": "integer",
            "description": "On the `other` node only — how many areas it folds."
          },
          "level_name": {
            "type": "string",
            "description": "The tenant's word for this tier, e.g. District."
          },
          "leaf": {
            "type": "boolean",
            "description": "True when this node is an individual store."
          },
          "store_count": {
            "type": "integer",
            "description": "Stores under this node."
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "percent": {
            "type": "integer"
          },
          "people_expected": {
            "type": "integer",
            "nullable": true,
            "description": "Audience size for an acknowledgement campaign; null otherwise."
          },
          "people_confirmed": {
            "type": "integer",
            "nullable": true,
            "description": "People who have acknowledged; null for non-acknowledgement work."
          },
          "inactive": {
            "type": "boolean",
            "description": "A deactivated location still holding work — listed so the level's rows add up to its parent's figures. Show an Inactive badge."
          }
        }
      },
      "CoverageDashboard": {
        "type": "object",
        "description": "The Coverage dashboard for one node and one lens.",
        "properties": {
          "location": {
            "type": "object",
            "nullable": true,
            "description": "The location this view is OF — the one every number in this response is bounded to. With `?node=` it is that node (a corporate, region, district or store). Without one it is the single top of the caller's span: a corporate member's corporate, a regional director's region, a district manager's district. Null for a node outside the span or another tenant's id (so a location's name never leaks past what the caller may open), and null when the span has several top locations (an admin with no location membership over a tenant with more than one root), where no single location is being viewed.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "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 or for a node outside it."
              },
              "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."
              },
              "tiers": {
                "type": "array",
                "description": "The structure beneath this view — one entry per location level, shallowest first, counting the child locations shown and everything under them inside the caller's span. A corporate view returns its regions, districts and stores; a region its districts and stores; a district its stores — so a client can frame \"3 Regions · 10 Districts · 66 Stores\". Labels are the tenant's own level names (Admin → Locations → Hierarchy → Levels). Active locations only; a location with no level is left out. Empty for a node outside the span.",
                "items": {
                  "type": "object",
                  "properties": {
                    "level_id": {
                      "type": "integer",
                      "description": "The LocationLevel id."
                    },
                    "level": {
                      "type": "integer",
                      "description": "The level's depth (0 is the top of the tenant's ladder)."
                    },
                    "label": {
                      "type": "string",
                      "description": "The level name exactly as the tenant set it",
                      "e.g. \"District\".": null
                    },
                    "label_plural": {
                      "type": "string",
                      "description": "The label's plural, e.g. \"Districts\"."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Active locations at this level beneath the view."
                    }
                  }
                },
                "example": [
                  {
                    "level_id": 12,
                    "level": 1,
                    "label": "Region",
                    "label_plural": "Regions",
                    "count": 3
                  },
                  {
                    "level_id": 13,
                    "level": 2,
                    "label": "District",
                    "label_plural": "Districts",
                    "count": 10
                  },
                  {
                    "level_id": 14,
                    "level": 3,
                    "label": "Store",
                    "label_plural": "Stores",
                    "count": 66
                  }
                ]
              }
            }
          },
          "header": {
            "type": "object",
            "description": "The headline numbers of the lens served — the same work in both lenses. The status figures and `percent` count obligations (done ÷ total), never people, so beside a post's card (`unit: people`) they count a post's store as done only once everyone there confirmed. Under a works council both lenses leave every post out of the status figures and say so (`posts_left_out`; in the campaign lens since 2026-10-31, before which it counted every post whose card showed figures). In the campaign lens `people_withheld` also says how many of those posts have their card withheld; the nodes lens never carries it.",
            "properties": {
              "percent": {
                "type": "integer"
              },
              "tier": {
                "type": "string",
                "enum": [
                  "good",
                  "warn",
                  "bad"
                ],
                "description": "Completion band (≥80 good, ≥40 warn, else bad — Execution::CoverageStatus)."
              },
              "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",
                "description": "Every active campaign in the view",
                "withheld posts included.": null
              },
              "scheduled_campaigns": {
                "type": "integer"
              },
              "people_withheld": {
                "type": "object",
                "description": "Knowledge Layer X3 (#4546) — campaign lens only: present when one or more active posts a works council covers hold fewer people than the company's smallest report group in this view; their items are not in the status figures above. Every post it counts is also counted by `posts_left_out`, which is always present beside it: a client showing `posts_left_out` need not show this too (each withheld card says so itself).",
                "properties": {
                  "reason": {
                    "type": "string",
                    "example": "Fewer than 5 people — counted in the level above."
                  },
                  "campaigns": {
                    "type": "integer",
                    "description": "How many active posts the figures leave out."
                  }
                }
              },
              "posts_left_out": {
                "type": "object",
                "description": "Finding #7955 — present in either lens (in the campaign lens since 2026-10-31; before, only in the nodes lens) while a works council is on and an active post reaches this view. The status figures above, and in the nodes lens every node card, then count no post (a location whose only active work is posts reads as one with no work); `active_campaigns` still counts them. A post's store item is done only when everyone at the store has confirmed, so a small store's count would say whether those few people had. Each post is its own card in the campaign lens, counted in people. `reason` is the sentence to show, worded for the lens served.",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Nodes lens: \"Posts are not counted in these figures. Each post is reported under By campaign.\" Campaign lens: \"Posts are not counted in these figures. Each post is reported on its own card.\"",
                    "example": "Posts are not counted in these figures. Each post is reported under By campaign."
                  },
                  "campaigns": {
                    "type": "integer",
                    "description": "How many active posts the status figures leave out."
                  }
                }
              }
            }
          },
          "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. While the header carries `posts_left_out`, no figure here counts a post.",
        "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"
          },
          "inactive": {
            "type": "boolean",
            "description": "A deactivated store that still holds work — listed so the level adds up; show an Inactive badge."
          }
        }
      },
      "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",
            "enum": [
              "normal",
              "important",
              "critical"
            ],
            "description": "The campaign's urgency band."
          },
          "work_type": {
            "type": "string",
            "enum": [
              "task",
              "inspection",
              "acknowledgement"
            ],
            "description": "What KIND of work this campaign is."
          },
          "work_type_label": {
            "type": "string",
            "description": "The floor-facing label for `work_type` — \"Task\", \"Inspection\" or \"Post\" (an acknowledgement)."
          },
          "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",
            "nullable": true,
            "description": "0–100, in the `unit` it names: for a post (`unit: people`) the share of its people in this view who confirmed it (`people.confirmed` ÷ `people.expected`), else done ÷ total — the figure the cards are ranked worst-first by, as the web Coverage list ranks them. Null while `people_withheld` is present."
          },
          "tier": {
            "type": "string",
            "nullable": true,
            "enum": [
              "good",
              "warn",
              "bad"
            ],
            "description": "The band of `percent`. Null while `people_withheld` is present."
          },
          "unit": {
            "type": "string",
            "enum": [
              "people",
              "locations"
            ],
            "description": "What `percent` counts — `people` for a post with work in this view, else `locations`. The status counts always count obligations. Additive."
          },
          "people": {
            "type": "object",
            "nullable": true,
            "description": "A post's \"N of M people confirmed\" pair in this view, the numbers `percent` is drawn from. Null for every other work type and while `people_withheld` is present. Additive.",
            "properties": {
              "expected": {
                "type": "integer"
              },
              "confirmed": {
                "type": "integer"
              }
            }
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer",
            "nullable": true,
            "description": "Null while `people_withheld` is present."
          },
          "in_progress": {
            "type": "integer",
            "nullable": true,
            "description": "Null while `people_withheld` is present."
          },
          "awaiting_review": {
            "type": "integer",
            "nullable": true,
            "description": "Obligations submitted and awaiting review — a SUBSET of in_progress, surfaced for the card's \"N awaiting review\" line. Null while `people_withheld` is present."
          },
          "not_started": {
            "type": "integer",
            "nullable": true,
            "description": "Null while `people_withheld` is present."
          },
          "missed": {
            "type": "integer",
            "nullable": true,
            "description": "Null while `people_withheld` is present."
          },
          "blocked": {
            "type": "integer"
          },
          "unassigned": {
            "type": "integer",
            "nullable": true,
            "description": "Null while `people_withheld` is present — open work with no holder is work those few have not finished."
          },
          "ready_to_close": {
            "type": "boolean",
            "nullable": true,
            "description": "Active and every obligation settled (done or missed). Null while `people_withheld` is present."
          },
          "people_withheld": {
            "type": "object",
            "description": "Knowledge Layer X3 (#4546) — present only on a post a works council covers whose audience in this view (the `node` drilled into, else the caller's span) holds fewer people than the company's smallest report group. Its status counts, unassigned, percent, tier, people and ready_to_close are null (a post's store is done only when everyone there confirmed), it is listed after every card that shows figures, by name, and `reason` is the sentence to show.",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Fewer than 5 people — counted in the level above."
              }
            }
          }
        }
      },
      "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."
          },
          "yours": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Areas whose nearest manager is the caller — nobody is nudged about them, and `message` says so."
          },
          "message": {
            "type": "string",
            "example": "Nudged 2 managers. 1 skipped — already nudged today, or no reachable manager. You manage North District yourself, so nobody was nudged about it."
          }
        },
        "required": [
          "nudged",
          "nudged_count",
          "skipped",
          "yours",
          "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 answer to a campaign-wide Remind, counted in people before anything is sent; the reminders themselves go out in the background.",
        "properties": {
          "reminded": {
            "type": "integer",
            "nullable": true,
            "description": "People the queued pass is reminding (within the 200-reminder cap). Null while `people_withheld` is present."
          },
          "skipped": {
            "type": "integer",
            "nullable": true,
            "description": "People reminded in the last 24 hours — every one of their obligations was — so they are not reminded again. Null while `people_withheld` is present."
          },
          "remaining": {
            "type": "integer",
            "nullable": true,
            "description": "People past the 200-reminder cap this pass will not reach — run Remind again to reach them. Null while `people_withheld` is present."
          },
          "queued": {
            "type": "boolean",
            "description": "Whether a pass was queued — true when it is reminding at least one person; false when nobody is outstanding or everyone outstanding was reminded in the last 24 hours."
          },
          "more_outstanding": {
            "type": "boolean",
            "description": "Present only with `people_withheld` — whether people are past the cap (run Remind again)."
          },
          "people_withheld": {
            "type": "object",
            "description": "Present only when the caller's whole span holds fewer people than\nthe company's smallest report group on a post a works council\ncovers: no count is given, and `message` says what the pass is\ndoing without one —\n\"Reminding the people who still owe it.\",\n\"Reminding the people who still owe it, except some reminded in the last 24 hours.\",\neither followed by \" Some are still outstanding — run Remind again to reach them.\"\nwhen people are past the cap, or\n\"Nobody new was reminded — the people who still owe it were reminded in the last 24 hours.\"\n",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Fewer than 5 people — counted in the level above."
              }
            }
          },
          "message": {
            "type": "string",
            "description": "A ready-to-show sentence, identical to the web Coverage notice:\n\"Reminding 3 people.\" plus \" 1 other person reminded in the last\n24 hours is skipped.\" when people were skipped and \" 2 more are\nstill outstanding — run Remind again to reach them.\" when people\nare past the cap; \"Everyone still outstanding was reminded in the\nlast 24 hours, so nobody was reminded again.\" when nobody new is\nreminded; \"Nobody has work outstanding on this campaign.\" when nobody is\noutstanding.\n",
            "example": "Reminding 3 people. 1 other person reminded in the last 24 hours is skipped."
          }
        },
        "required": [
          "reminded",
          "skipped",
          "remaining",
          "queued",
          "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 while the work is open again after a send-back (reopened, or handed on before it was redone) — 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"
          },
          "withdrawn": {
            "type": "boolean",
            "example": false,
            "description": "True when this work was withdrawn before anyone could start it — its shift was cancelled or reassigned, or its campaign version was replaced. Its status stays `missed`, but it counts as neither done nor missed in any coverage figure."
          },
          "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
          },
          "inspection_id": {
            "type": "integer",
            "nullable": true,
            "example": 4821,
            "description": "The Inspection this item minted when the campaign's kind of work is inspection; null otherwise. Open it to do the work — Mark done is refused for inspection work."
          },
          "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 while the work is open again after a send-back: reopened, or handed on or released before it was redone, so the next person still sees what to fix."
          },
          "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
              },
              "resolved_by_name": {
                "type": "string",
                "nullable": true,
                "description": "Who answered the challenge, by name (\"Deleted user\" once their account is gone) — render the web's \"<resolved_by_name> kept this with you: <reason>\", never \"Your manager\": the answer can come from a district manager or an admin. Null while open."
              }
            }
          },
          "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",
                "description": "Photos that count as this attempt's proof. Photos from before a send-back are history, not proof, and are counted in `earlier_attempt_photos` instead.",
                "example": 2
              },
              "earlier_attempt_photos": {
                "type": "integer",
                "example": 1
              },
              "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."
              }
            }
          }
        }
      },
      "FrontlineExecutionLocationsResult": {
        "type": "object",
        "description": "The paginated store list plus the standard pagination meta.",
        "properties": {
          "locations": {
            "type": "array",
            "description": "The active physical sites, ordered by name.",
            "items": {
              "$ref": "#/components/schemas/FrontlineExecutionLocation"
            }
          },
          "meta": {
            "type": "object",
            "description": "Standard pagination envelope.",
            "properties": {
              "total_count": {
                "type": "integer",
                "example": 128
              },
              "total_pages": {
                "type": "integer",
                "example": 6
              },
              "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
              }
            }
          }
        },
        "required": [
          "locations",
          "meta"
        ]
      },
      "FrontlineExecutionLocation": {
        "type": "object",
        "description": "One selectable store for the \"Which locations\" picker.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 55
          },
          "name": {
            "type": "string",
            "example": "Fourth Street"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "MyDayListItem": {
        "type": "object",
        "description": "One My Day obligation (Execution::CampaignItem), with the fields the My Day row shows, including the \"I can't do this\" flag and the note the worker left their manager with it. A lean, list-optimised shape — the fuller single-item shape (assignment explanation, challenge) 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,
            "description": "What is blocking it — the reason the worker picked when flagging. Null when not flagged.",
            "enum": [
              "no_stock",
              "missing_fixture_or_supplies",
              "equipment_down",
              "not_enough_time",
              "unclear_instructions",
              "other"
            ]
          },
          "blocked_reason_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for `blocked_reason`. Null when not flagged.",
            "example": "Equipment is down"
          },
          "blocked_note": {
            "type": "string",
            "nullable": true,
            "maxLength": 500,
            "description": "The note the worker left for their manager when flagging it (\"Anything your manager should know\"). Optional when flagging, so it can be null on a flagged row; always null on an unflagged one. A client renders the web's \"Flagged: <label> — <note>\" line from `blocked_reason_label` + `blocked_note`.",
            "example": "Walk-in cooler alarm going off, can't restock until it's fixed"
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it was flagged. Null when not flagged."
          },
          "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",
              "onboarding",
              "signatures"
            ],
            "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"
          },
          "mobile_url": {
            "type": "string",
            "nullable": true,
            "description": "The row's phone page, where it is not simply the phone form of `url` (an onboarding form). Null otherwise."
          },
          "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."
          },
          "blocked": {
            "type": "boolean",
            "description": "Whether the worker has flagged this as \"I can't do this\". Only a `frontline` row can be flagged; every other source is always false.",
            "example": true
          },
          "blocked_reason": {
            "type": "string",
            "nullable": true,
            "description": "What is blocking it — the reason the worker picked when flagging. Null when not flagged.",
            "enum": [
              "no_stock",
              "missing_fixture_or_supplies",
              "equipment_down",
              "not_enough_time",
              "unclear_instructions",
              "other"
            ],
            "example": "missing_fixture_or_supplies"
          },
          "blocked_reason_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for `blocked_reason`. Null when not flagged.",
            "example": "Missing fixture or supplies"
          },
          "blocked_note": {
            "type": "string",
            "nullable": true,
            "maxLength": 500,
            "description": "The note the worker left for their manager when flagging it (\"Anything your manager should know\"). Optional when flagging, so it can be null on a flagged row; always null on an unflagged one.",
            "example": "Promo signage never arrived in the delivery"
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it was flagged. Null when not flagged."
          }
        }
      },
      "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"
          },
          "acknowledgeable": {
            "type": "boolean",
            "description": "Whether THIS caller may acknowledge it — false once acknowledged, for its\nauthor, and for anyone who worked a shift at this store ending within 30\nminutes of it.\n"
          },
          "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"
              },
              "photo_url": {
                "type": "string",
                "description": "The author's avatar URL — their uploaded profile photo when they have one, otherwise a generated initials avatar. Always present and absolute, so a native thread can show a face beside each post.",
                "example": "https://example.mangoapps.com/rails/active_storage/representations/redirect/eyJ.../avatar.jpg"
              }
            }
          },
          "parent_id": {
            "type": "integer",
            "nullable": true,
            "description": "The question this replies to; null for a top-level question.",
            "example": null
          },
          "is_campaign_owner": {
            "type": "boolean",
            "description": "True when the author is the person who owns (created) the campaign — the web thread's \"Campaign owner\" badge, which marks an answer as authoritative.",
            "example": false
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "edited": {
            "type": "boolean",
            "description": "True when the comment was edited after it was posted.",
            "example": false
          },
          "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"
          },
          "photo_url": {
            "type": "string",
            "description": "The person's avatar URL — their uploaded profile photo when they have one, otherwise a generated initials avatar. Always present and absolute, so a native picker can show a face beside the name.",
            "example": "https://example.mangoapps.com/rails/active_storage/representations/redirect/eyJ.../avatar.jpg"
          },
          "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",
          "photo_url",
          "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`, `invalid`, or (withdraw only) `forbidden`.",
                "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
          },
          "target_locations": {
            "type": "array",
            "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 42
                },
                "name": {
                  "type": "string",
                  "example": "Store 042 - Downtown"
                }
              }
            }
          },
          "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/..."
                }
              }
            }
          }
        }
      },
      "FrontlineExecutionRequestFormConfig": {
        "type": "object",
        "description": "The intake-request form's option lists.",
        "properties": {
          "categories": {
            "type": "array",
            "description": "The active categories offered by the form's category dropdown, ordered by (sort_order, name).",
            "items": {
              "$ref": "#/components/schemas/FrontlineExecutionFormCategory"
            }
          }
        },
        "required": [
          "categories"
        ]
      },
      "FrontlineExecutionFormCategory": {
        "type": "object",
        "description": "One selectable category for the intake-request form's category dropdown.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 12
          },
          "name": {
            "type": "string",
            "example": "Planogram"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Optional helper text authored alongside the category; null when none was set.",
            "example": "Weekly planogram resets"
          },
          "color_token": {
            "type": "string",
            "nullable": true,
            "description": "Bootstrap subtle-badge token for the category chip; null when none was set.",
            "example": "info"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "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
              },
              "target_locations": {
                "type": "array",
                "description": "The stores the requester named directly, resolved to id + name and ordered by name. Empty when the request names an audience or every location instead of a direct list. Tenant-scoped — a store id from another business is never surfaced here.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 42
                    },
                    "name": {
                      "type": "string",
                      "example": "Store 042 - Downtown"
                    }
                  }
                }
              },
              "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"
          },
          "from_request_id": {
            "type": "integer",
            "description": "Ask again: the caller's own DECLINED request this one re-sends. Every field this call leaves out is taken from it (a field sent as null is cleared), as is its store list when no store input is sent, and its files are attached again. An id that is not the caller's declined request is refused (`invalid_from_request`, 422)."
          },
          "carry_attachment_ids": {
            "type": "array",
            "description": "With `from_request_id`: which of the declined request's files to attach again (its attachment ids). Leave it out to carry all of them; an empty list carries none.",
            "items": {
              "type": "integer"
            }
          }
        }
      },
      "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`, `self_review` or `campaign_closed` (reject only).",
                "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
          },
          "sent_back": {
            "type": "object",
            "nullable": true,
            "description": "Present when this submission is the redo of work that was sent back for rework — the reason it was sent back and when — so a client can show the reviewer what was asked for beside the new proof. Null for a first submission.",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Bay 2 still shows last week's prices — re-shoot it."
              },
              "sent_back_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "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. Advisory only: it never accepts or sends back work.",
            "properties": {
              "verdict": {
                "type": "string",
                "enum": [
                  "looks_good",
                  "concerns",
                  "unclear"
                ],
                "description": "`unclear` — the check couldn't tell from the photo. Checks stored before it existed read `concerns` for both.",
                "example": "looks_good"
              },
              "looks_good": {
                "type": "boolean",
                "example": true
              },
              "summary": {
                "type": "string",
                "example": "Shelves match the planogram; facings are full."
              },
              "compared_with_reference": {
                "type": "boolean",
                "nullable": true,
                "description": "True when the photo was compared with the campaign's planogram image, false when checked against its instructions only, null for a check stored before this was recorded."
              },
              "reference": {
                "type": "string",
                "nullable": true,
                "example": "Compared with the campaign's planogram image."
              },
              "advisory": {
                "type": "string",
                "example": "A suggestion; the reviewer decides."
              },
              "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 count of this attempt's photos (>= shown.size). On a redo, the photos from before the send-back are not counted here — they are in `earlier_attempt`.",
            "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."
                }
              }
            }
          },
          "earlier_attempt": {
            "type": "array",
            "description": "On a redo, the photos from before the work was sent back (the ones the reviewer turned down), most recent last and capped like `shown` — history to compare against, not this submission's proof. Empty for a first submission.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 55002
                },
                "filename": {
                  "type": "string",
                  "example": "endcap-left-before.jpg"
                },
                "url": {
                  "type": "string"
                },
                "thumb_url": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "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",
            "nullable": true,
            "description": "How many were signed off (done). Null while `people_withheld` is present.",
            "example": 37
          },
          "missed": {
            "type": "integer",
            "nullable": true,
            "description": "How many were missed (window closed). Null while `people_withheld` is present.",
            "example": 3
          },
          "people_withheld": {
            "type": "object",
            "description": "Knowledge Layer X3 (#4546) — present only on a post a works council covers whose caller's whole span holds fewer people than the company's smallest report group; `done` and `missed` are then null.",
            "properties": {
              "reason": {
                "type": "string",
                "example": "Fewer than 5 people — counted in the level above."
              }
            }
          }
        }
      },
      "IntelligenceSmartDefaults": {
        "type": "object",
        "description": "The caller's smart defaults. Keys after `source` depend on the time of day, the request's category and assistant type, and the caller's recent sessions; absent keys simply did not apply.",
        "required": [
          "ai_assistant",
          "communication_style",
          "complexity_preference",
          "session_duration",
          "preferred_categories",
          "peak_usage_hours",
          "confidence_score",
          "source",
          "time_context",
          "suggested_session_length",
          "confidence_factors",
          "generated_at",
          "expires_at",
          "user_profile_version",
          "metadata"
        ],
        "properties": {
          "ai_assistant": {
            "type": "string",
            "example": "task_assistant"
          },
          "communication_style": {
            "type": "string",
            "example": "moderate"
          },
          "complexity_preference": {
            "type": "string",
            "example": "moderate"
          },
          "session_duration": {
            "type": "string",
            "example": "medium"
          },
          "preferred_categories": {
            "type": "array",
            "items": {}
          },
          "peak_usage_hours": {
            "type": "array",
            "description": "Hours of the day (0-23).",
            "items": {
              "type": "integer"
            }
          },
          "confidence_score": {
            "type": "string",
            "description": "Overall confidence 0..1, the mean of `confidence_factors`. A decimal, rendered as a string.",
            "example": "0.25"
          },
          "source": {
            "type": "string",
            "enum": [
              "postgresql_materialized_view",
              "fallback_defaults"
            ]
          },
          "time_context": {
            "type": "string",
            "enum": [
              "peak_productivity",
              "off_peak"
            ]
          },
          "suggested_session_length": {
            "type": "string",
            "enum": [
              "longer",
              "shorter"
            ]
          },
          "workload_context": {
            "type": "string",
            "enum": [
              "high",
              "fresh"
            ],
            "description": "`high` after more than 3 sessions in the last hour, `fresh` after none; absent otherwise."
          },
          "suggested_complexity": {
            "type": "string"
          },
          "category_complexity": {
            "type": "string"
          },
          "category_duration": {
            "type": "string"
          },
          "category_style": {
            "type": "string"
          },
          "category_confidence": {
            "type": "number"
          },
          "assistant_satisfaction": {
            "type": "string",
            "description": "Decimal, as a string."
          },
          "assistant_usage_frequency": {
            "type": "number"
          },
          "assistant_preferred_complexity": {
            "type": "string"
          },
          "assistant_confidence": {
            "type": "number"
          },
          "focus_areas": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "suggested_prompts": {
            "type": "array",
            "description": "Up to three `[name, prompt_content]` pairs.",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "interaction_style": {
            "type": "string",
            "enum": [
              "action_oriented",
              "guidance_focused",
              "analytical",
              "detailed_technical"
            ]
          },
          "field_assistance_level": {
            "type": "string",
            "enum": [
              "minimal",
              "moderate",
              "extensive"
            ],
            "description": "`form_assistant` only."
          },
          "query_complexity": {
            "type": "string",
            "description": "`database_assistant` only."
          },
          "safety_level": {
            "type": "string",
            "enum": [
              "high"
            ],
            "description": "`admin_assistant` only."
          },
          "confidence_factors": {
            "type": "object",
            "properties": {
              "data_volume": {
                "type": "number"
              },
              "pattern_consistency": {
                "type": "string",
                "description": "Decimal, as a string."
              },
              "recent_activity": {
                "type": "number"
              },
              "context_match": {
                "type": "number"
              }
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "user_profile_version": {
            "type": "string",
            "format": "date-time",
            "description": "When the caller's intelligence profile was last updated."
          },
          "metadata": {
            "type": "object",
            "properties": {
              "total_interactions": {
                "type": "integer"
              },
              "pattern_confidence": {
                "type": "string",
                "description": "Decimal, as a string."
              },
              "last_analysis": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "generation_method": {
                "type": "string",
                "example": "postgresql_native_with_context"
              }
            }
          }
        }
      },
      "IntelligenceContextualSuggestion": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "approach_suggestion",
              "response_template",
              "follow_up",
              "goal"
            ],
            "example": "follow_up"
          },
          "content": {
            "description": "The text the person could send: an approach or response template from one of their successful patterns, or a follow-up.",
            "example": "Walk me through it step by step"
          },
          "confidence": {
            "type": "number"
          },
          "source": {
            "type": "string",
            "enum": [
              "similar_pattern",
              "successful_pattern",
              "conversation_context",
              "target_outcome"
            ]
          },
          "relevance": {
            "type": "number",
            "description": "Present on follow-ups; the relevance the ranking used."
          },
          "pattern_id": {
            "type": "integer",
            "description": "The interaction pattern a suggestion came from (pattern suggestions only)."
          },
          "relevance_score": {
            "type": "number"
          },
          "combined_score": {
            "type": "number",
            "description": "0.6 × relevance + 0.4 × confidence; the list is sorted by it, highest first."
          },
          "ranking_factors": {
            "type": "object",
            "properties": {
              "context_match": {
                "type": "number"
              },
              "pattern_confidence": {
                "type": "number"
              },
              "user_preference_alignment": {
                "type": "number"
              }
            }
          }
        }
      },
      "IntelligencePromptSuggestion": {
        "type": "object",
        "required": [
          "id",
          "title",
          "content",
          "category",
          "complexity",
          "intelligence_score",
          "effectiveness_rating",
          "personalization_score",
          "estimated_tokens",
          "success_rate"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "complexity": {
            "type": "string",
            "nullable": true
          },
          "intelligence_score": {
            "description": "The template's intelligence score."
          },
          "effectiveness_rating": {
            "description": "The template's effectiveness rating."
          },
          "personalization_score": {
            "type": "number"
          },
          "estimated_tokens": {
            "type": "integer"
          },
          "success_rate": {
            "description": "The template's recorded success rate, when it has one."
          }
        }
      },
      "IntelligenceBusinessPerformance": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "integer"
          },
          "business_name": {
            "type": "string"
          },
          "total_users": {
            "type": "integer",
            "description": "People with an intelligence profile in the business."
          },
          "active_users": {
            "type": "integer",
            "description": "Of those, interacted in the last 7 days."
          },
          "avg_intelligence_score": {
            "description": "Decimal as a string, or the number 0 when there are no profiles."
          },
          "total_interactions": {
            "type": "integer"
          },
          "avg_satisfaction": {
            "description": "Decimal as a string, or the number 0 when no profile has a score."
          },
          "patterns_count": {
            "type": "integer"
          },
          "sessions_count": {
            "type": "integer"
          },
          "successful_sessions": {
            "type": "integer"
          },
          "recent_activity": {
            "type": "object",
            "properties": {
              "sessions_last_24h": {
                "type": "integer"
              },
              "patterns_last_24h": {
                "type": "integer"
              },
              "active_users_last_24h": {
                "type": "integer"
              }
            }
          },
          "performance_trends": {
            "type": "array",
            "description": "One row per day for the last seven days.",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string"
                },
                "sessions": {
                  "type": "integer"
                },
                "successful_sessions": {
                  "type": "integer"
                },
                "avg_satisfaction": {}
              }
            }
          }
        }
      },
      "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 someone who can manage the library (a Libraries admin, a place-limited one in reach, its creator or a named `manager`) ever receives a `false` here.",
            "example": true
          },
          "can_disable": {
            "type": "boolean",
            "description": "Whether this caller may take the availability verb THIS row offers: Disable on an enabled library (`POST /libraries/{id}/disable`, which asks a token `destructive:libraries`) or Enable on a disabled one (`POST /libraries/{id}/enable`, which asks `write:libraries`). False for anyone who is not a Libraries admin or a place-limited Libraries admin with this library in reach. So a token holding `write:libraries` alone sees true only on disabled rows.",
            "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."
          },
          "captions": {
            "type": "string",
            "nullable": true,
            "description": "The listing's captions marker for a VIDEO, the state the web and phone listings draw their \"CC\" chip from. `available` — the video has a caption track (draw \"CC\", spoken label \"Captions available\"); `coming` — a track is being generated (draw \"Captions coming\"). Null for every other item, and for a video with neither."
          },
          "captions_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of that video's WebVTT caption track — fetch it with the same bearer token (GET /libraries/{library_id}/items/{id}/captions, `text/vtt`) and show the cues while the video plays. Set exactly when `captions` is `available`; null otherwise. The web player's `<track>` route answers only a browser session, so a native client must use this URL."
          },
          "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
          },
          "can_delete": {
            "type": "boolean",
            "description": "May this caller delete the item from the 3-dot sheet: `can_manage` AND the token holds `destructive:libraries`, which `DELETE /libraries/{library_id}/items/{id}` asks. Draw Delete off this flag; Edit and Move stay on `can_manage` (web handoffs).",
            "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 (a Libraries admin, or a place-limited one with this library in reach) — the authority `POST /libraries/{id}/disable` and `…/enable` gate on, AND the scope of the verb this library offers: Disable asks a token `destructive:libraries`, Enable asks `write:libraries`."
          },
          "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)."
          },
          "captions": {
            "type": [
              "string",
              "null"
            ],
            "description": "The listing's captions marker for a VIDEO, the state the web and phone listings draw their \"CC\" chip from. `available` — the video has a caption track (draw \"CC\", spoken label \"Captions available\"); `coming` — a track is being generated (draw \"Captions coming\"). Null for every other item, and for a video with neither."
          },
          "captions_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute URL of that video's WebVTT caption track — fetch it with the same bearer token (GET /libraries/{library_id}/items/{id}/captions, `text/vtt`) and show the cues while the video plays. Set exactly when `captions` is `available`; null otherwise. The web player's `<track>` route answers only a browser session, so a native client must use this URL."
          },
          "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"
          }
        }
      },
      "LiveAssistSession": {
        "type": "object",
        "required": [
          "id",
          "mode",
          "status",
          "provider",
          "sessionable",
          "host",
          "recording_status",
          "ai_summary_status",
          "ai_summary",
          "started_at",
          "ended_at",
          "duration_seconds",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "mode": {
            "type": "string",
            "enum": [
              "call_for_help",
              "show_me",
              "watch",
              "huddle"
            ]
          },
          "mode_label": {
            "type": "string",
            "description": "What the web app calls the mode — Help call, Live video, Huddle or Site feed; Remote support for a call a Service Desk agent placed to a ticket's requester (its `mode` is the help mode it rides on).",
            "example": "Remote support"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "live",
              "ended"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "What the web app shows for the status — Not connected (pending), Live or Ended.",
            "example": "Live"
          },
          "provider": {
            "type": "string",
            "enum": [
              "livekit"
            ]
          },
          "sessionable": {
            "type": "object",
            "description": "The work object the session is stapled to.",
            "required": [
              "type",
              "id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "description": "Polymorphic class name.",
                "example": "FieldService::WorkOrder"
              },
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "description": "The record's own name (a work order's title, an inspection's name); null when it has none or is gone."
              },
              "label": {
                "type": "string",
                "description": "The label the web lists print — the record's name and its reference.",
                "example": "Pump 3 repair — Work Order WO-00012"
              }
            }
          },
          "answered_by": {
            "type": "object",
            "nullable": true,
            "description": "The responder who has answered a live help call (\"… is on it\"); null when nobody has, the mode is not answered, or the session has ended.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "description": "The responder's name as every web surface prints it (full name, else display name); their email where the directory shows emails, else \"Unnamed user\". Never null."
              }
            }
          },
          "host": {
            "type": "object",
            "nullable": true,
            "description": "Who started the session; null when it has no host.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "description": "The host's name as every web surface prints it (full name, else display name); their email where the directory shows emails, else \"Unnamed user\". Never null."
              }
            }
          },
          "recording_status": {
            "type": "string",
            "enum": [
              "none",
              "recording",
              "processing",
              "ready",
              "failed"
            ]
          },
          "ai_summary_status": {
            "type": "string",
            "enum": [
              "none",
              "processing",
              "ready",
              "failed"
            ]
          },
          "ai_summary": {
            "type": "string",
            "nullable": true,
            "description": "The AI summary of the recording, once one has been written."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null until the session goes live."
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_seconds": {
            "type": "integer",
            "description": "Seconds from `started_at` to `ended_at` (or to now while the session is live); 0 when it never started."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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. Null for a native-source reading, and when the caller may not open the source table."
          },
          "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 visualization is held on screen (15–600)."
          },
          "visualization_ids": {
            "type": "array",
            "description": "The rotation's visualizations, in play order — the ids the rotation was authored with, including any since archived or restricted (`boards` on the detail endpoint omits those).",
            "items": {
              "type": "integer"
            }
          },
          "board_ids": {
            "type": "array",
            "deprecated": true,
            "description": "Deprecated alias of `visualization_ids`: the same visualization ids (not board ids), kept for existing clients.",
            "items": {
              "type": "integer"
            }
          },
          "shared": {
            "type": "boolean",
            "description": "Whether sharing is switched on for this playlist. Says nothing about whether the link opens right now — see link_state."
          },
          "link_state": {
            "type": "string",
            "enum": [
              "live",
              "off",
              "archived",
              "public_links_off"
            ],
            "description": "Whether the rotation link opens right now: live; off (sharing is switched off for this playlist); archived (the playlist is archived); public_links_off (public board links are switched off for the whole workspace). Says nothing about individual visualizations."
          },
          "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. Also returned for an archived playlist and while public links are off — check link_state."
          },
          "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 visualizations allows it; if none of them restrict framing, any site may.",
            "items": {
              "type": "string"
            }
          },
          "boards": {
            "type": "array",
            "description": "Detail endpoint only — the rotation's visualizations the caller may see, in play order (named `boards` for existing clients).",
            "items": {
              "$ref": "#/components/schemas/LiveBoardReading"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LocationDepartmentsDepartment": {
        "type": "object",
        "required": [
          "id",
          "name",
          "department_type",
          "department_type_display",
          "description",
          "active"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "department_type": {
            "type": "string",
            "nullable": true,
            "description": "The department's kind. The app offers floor, wing, unit, section, department, division, area, zone, team and support, but the column is not constrained to that list; null when unset.",
            "example": "floor"
          },
          "department_type_display": {
            "type": "string",
            "description": "`department_type` titleized for display (e.g. \"Floor\"); \"General\" when `department_type` is null.",
            "example": "Floor"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "description": "Always true here — inactive departments are not listed."
          }
        }
      },
      "AssignedLocation": {
        "type": "object",
        "required": [
          "id",
          "name",
          "enforce_geofence",
          "is_primary"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "enforce_geofence": {
            "type": "boolean",
            "description": "True when clock-ins at this location are checked against its geofence."
          },
          "allowed_radius_meters": {
            "type": "integer",
            "nullable": true,
            "description": "Geofence radius; null when the location has no geofence."
          },
          "is_primary": {
            "type": "boolean",
            "description": "True for the user's primary location (at most one in the list)."
          }
        }
      },
      "LoneWorkerPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "LoneWorkerAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "trigger_type": {
            "type": "string",
            "enum": [
              "sos",
              "missed_check_in",
              "geofence",
              "no_checkout",
              "line_jam"
            ]
          },
          "trigger_note": {
            "type": "string",
            "nullable": true,
            "description": "The worker's own description of what is wrong (280 characters at most)."
          },
          "trigger_event": {
            "type": "string",
            "nullable": true,
            "description": "The device event that raised the alert (wearables only, e.g. fall, man_down, panic). Null for an alert a person raised in the app."
          },
          "detection_label": {
            "type": "string",
            "nullable": true,
            "description": "\"Fall detected\" or \"Man-down detected\" when a device detected distress on its own; null when a person pressed SOS."
          },
          "unassigned_device": {
            "type": "boolean",
            "description": "True when a device with no badge (or an unknown one) raised it. The alert is then filed under the wearables integration's fallback person, who is not the one in distress."
          },
          "state": {
            "type": "string",
            "enum": [
              "raised",
              "notifying",
              "acknowledged",
              "resolved",
              "exhausted"
            ]
          },
          "current_tier": {
            "type": "integer"
          },
          "worker": {
            "$ref": "#/components/schemas/LoneWorkerPerson"
          },
          "shift_id": {
            "type": "integer",
            "nullable": true
          },
          "roster": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "scope": {
                "type": "string"
              }
            }
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "lat": {
                "type": "number"
              },
              "lng": {
                "type": "number"
              },
              "accuracy_m": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "raised_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "acknowledged_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LoneWorkerPerson"
              }
            ],
            "nullable": true
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ack_resumes_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an acknowledgement with no outcome recorded goes back to its tier and pages it again (never while Lone Worker is switched off); null unless acknowledged, while the app is switched off, and for sample alerts."
          },
          "exhaustion_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "unanswered",
              "lapsed",
              "abandoned",
              "handed_back",
              "unstaffed",
              null
            ],
            "description": "Why an exhausted alert ran out, on GET /alerts/{id} (and safety_status.exhausted_alert): nobody acknowledged, an acknowledgement lapsed, was left open, was handed back, or nobody could be alerted. Omitted from lists."
          },
          "resolution": {
            "type": "string",
            "nullable": true,
            "enum": [
              "safe",
              "false_alarm",
              "incident",
              null
            ]
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolution_note": {
            "type": "string",
            "nullable": true
          },
          "incident_id": {
            "type": "integer",
            "nullable": true
          },
          "is_sample_data": {
            "type": "boolean",
            "description": "True for a seeded demo alert — render it as sample data",
            "never as a real emergency.": null
          }
        }
      },
      "LoneWorkerAlertEvent": {
        "type": "object",
        "properties": {
          "event_type": {
            "type": "string",
            "enum": [
              "raised",
              "tier_notified",
              "notification_sent",
              "acknowledged",
              "released",
              "resolved",
              "exhausted",
              "cancelled",
              "recovered",
              "incident_handoff_failed",
              "check_in_missed"
            ]
          },
          "label": {
            "type": "string",
            "description": "The row in words, as the web trail shows it (\"Handed back\", \"Acknowledgement lapsed — back on its tier\", \"Check-in missed during this alert\"). Show this, not `event_type`."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "tier": {
            "type": "integer",
            "nullable": true,
            "description": "The ladder rung. Roster tiers start at 1; 0 is the break-glass rung, the safety admins paged when no roster covered the alert or nobody on it could be paged."
          },
          "channel": {
            "type": "string",
            "nullable": true
          },
          "actor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LoneWorkerPerson"
              }
            ],
            "nullable": true
          },
          "recipient": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LoneWorkerPerson"
              }
            ],
            "nullable": true,
            "description": "Who a `notification_sent` row paged."
          },
          "via": {
            "type": "string",
            "nullable": true,
            "description": "The door an acknowledgement came through (web, api, agent, inbox, voice), or `device` on a `raised` row a wearable raised."
          },
          "delivery": {
            "type": "string",
            "nullable": true,
            "description": "`notification_sent` only — `requested`, `skipped` or `error`."
          }
        }
      },
      "LoneWorkerAlertDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LoneWorkerAlert"
          },
          {
            "type": "object",
            "properties": {
              "resolved_by": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LoneWorkerPerson"
                  }
                ],
                "nullable": true
              },
              "events": {
                "type": "array",
                "maxItems": 50,
                "description": "The newest trail rows, oldest first.",
                "items": {
                  "$ref": "#/components/schemas/LoneWorkerAlertEvent"
                }
              },
              "events_total": {
                "type": "integer"
              },
              "events_truncated": {
                "type": "boolean",
                "description": "True when the alert has more trail rows than `events` carries."
              },
              "safety_admin_fallback": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "no_roster",
                  "nobody_pageable",
                  null
                ],
                "description": "Set when the safety admins were paged instead of a roster's responders: `no_roster` (no roster covered the shift when the alert was raised) or `nobody_pageable` (a roster resolved but nobody on it could be paged). Null otherwise."
              }
            }
          }
        ]
      },
      "LoneWorkerCheckIn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "armed",
              "ok",
              "missed",
              "cancelled"
            ]
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "responded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "shift_id": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "LoneWorkerRaiseResult": {
        "type": "object",
        "properties": {
          "alert": {
            "$ref": "#/components/schemas/LoneWorkerAlert"
          },
          "reached_responders": {
            "type": "boolean",
            "description": "Absent on the `already_active` reply. False means nobody was paged."
          },
          "already_active": {
            "type": "boolean",
            "description": "Present (true) only when an SOS was already in progress."
          },
          "message": {
            "type": "string",
            "description": "Present only when no responder could be reached."
          }
        }
      },
      "MangoSignalAction": {
        "type": "object",
        "description": "An action. `scope_type`, `segment_key`, `scope_id` (null with a `scope_label` for an action about one individual) and `signal_ids` appear only for a manager-tier caller; `description`, `completion_notes`, `source_survey`, `signal_ids` and `impact` only on a single action (GET or POST/PATCH by id).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "in_progress",
              "complete",
              "dismissed"
            ]
          },
          "assignee_id": {
            "type": "integer",
            "nullable": true
          },
          "assignee_name": {
            "type": "string",
            "nullable": true
          },
          "assignee_active": {
            "type": "boolean"
          },
          "owner_id": {
            "type": "integer"
          },
          "owner_name": {
            "type": "string",
            "nullable": true
          },
          "owner_active": {
            "type": "boolean"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The due day, stored as noon UTC of that day."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The day it is due (`YYYY-MM-DD`) — the same day in every time zone."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ai_recommended": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "is_sample_data": {
            "type": "boolean"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "completion_notes": {
            "type": "string",
            "nullable": true
          },
          "source_survey": {
            "type": "object",
            "nullable": true
          },
          "impact": {
            "type": "object",
            "nullable": true
          },
          "scope_type": {
            "type": "string",
            "nullable": true
          },
          "segment_key": {
            "type": "string",
            "nullable": true
          },
          "scope_id": {
            "type": "integer",
            "nullable": true
          },
          "scope_label": {
            "type": "string"
          },
          "signal_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only when a submitted `scope_id` was dropped."
          }
        }
      },
      "MangoSignalRefusal": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "action_access_required"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "field": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "valid_statuses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "MangoSpendExpenseReport": {
        "type": "object",
        "description": "An expense report. `purpose`, `lines`, `violations` and `standing` appear only in the detailed form — the answers of create, show, submit, approve, reject and request_changes; the lists (`expense_reports`, `approvals`) and the line-removal answer carry the short form.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "number": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "approved",
              "rejected",
              "sent_back",
              "paid",
              "cancelled"
            ],
            "description": "`sent_back` is shown to people as \"Changes Requested\"."
          },
          "status_label": {
            "type": "string",
            "description": "The status as every page and email words it (\"Changes Requested\" for `sent_back`)."
          },
          "currency": {
            "type": "string"
          },
          "total_cents": {
            "type": "integer"
          },
          "reimbursable_cents": {
            "type": "integer"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "paid_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "The web page to send the caller to for this report (a path on the tenant's host): the reimbursements page for the owner, their manager and Mango Spend admins, the approval review page for an approver named only in the report's chain."
          },
          "purpose": {
            "type": "string",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MangoSpendExpenseLine"
            }
          },
          "violations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MangoSpendPolicyViolation"
            }
          },
          "standing": {
            "type": "object",
            "description": "Where the report stands, as its page says it. Each key appears only when it applies.",
            "properties": {
              "decision": {
                "type": "object",
                "description": "The last decision on an approved, rejected, sent-back or paid report.",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "rejected",
                      "sent_back"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Approved\", \"Rejected\" or \"Changes Requested\"."
                  },
                  "decided_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "decided_by": {
                    "type": "string",
                    "description": "The approver's name. Absent when they have none on file."
                  },
                  "decision_note": {
                    "type": "string",
                    "description": "The approver's reason",
                    "in their words.": null
                  }
                }
              },
              "waiting_on": {
                "type": "object",
                "description": "Who a submitted report is waiting on.",
                "properties": {
                  "names": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The approver, or the Mango Spend admins it went to (up to three)."
                  },
                  "nobody": {
                    "type": "boolean",
                    "description": "True when no one can review it yet."
                  },
                  "sentence": {
                    "type": "string"
                  }
                }
              },
              "payment": {
                "type": "object",
                "description": "For an approved or paid report — its payment batch and pay date.",
                "properties": {
                  "batch_number": {
                    "type": "string"
                  },
                  "batch_status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "exported",
                      "sent_to_payroll",
                      "paid",
                      "failed",
                      "voided",
                      "cancelled"
                    ]
                  },
                  "batch_status_label": {
                    "type": "string",
                    "description": "The batch's state as the Payments page words it."
                  },
                  "batch_url": {
                    "type": "string",
                    "description": "The batch's page — only for a caller who can run payments."
                  },
                  "pay_date": {
                    "type": "string",
                    "format": "date",
                    "description": "The pay date the batch's payment file carries."
                  },
                  "paid_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "sentence": {
                    "type": "string",
                    "description": "The report page's sentence for this state."
                  }
                }
              }
            }
          }
        }
      },
      "MangoSpendExpenseLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "line_type": {
            "type": "string",
            "enum": [
              "expense",
              "mileage",
              "per_diem"
            ]
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "The category's name."
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "vendor": {
            "type": "string",
            "nullable": true
          },
          "spent_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "amount_cents": {
            "type": "integer"
          },
          "tax_cents": {
            "type": "integer"
          },
          "miles": {
            "type": "number"
          },
          "currency": {
            "type": "string",
            "description": "The report's currency."
          },
          "needs_amount": {
            "type": "boolean",
            "description": "The line has no money on it yet (amount of zero). Submitting is refused until it is priced."
          },
          "needs_amount_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why the line has no amount and who fixes it — the sentence its row on the report page shows (for example, no mileage rate covers its date). Null when the line has an amount."
          },
          "receipts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "status": {
                  "type": "string",
                  "description": "The receipt reader's progress with the file."
                }
              }
            }
          }
        }
      },
      "MangoSpendLineInput": {
        "type": "object",
        "description": "The fields a client may set on a line; any other key is ignored. `currency` and the mileage and per-diem rates are not among them — the line takes the report's currency and the business's rates.",
        "properties": {
          "line_type": {
            "type": "string",
            "enum": [
              "expense",
              "mileage",
              "per_diem"
            ],
            "description": "`mileage` needs `miles` above zero; `per_diem` needs `days` above zero and a `location_code`. The line's own rules apply on top (see the 422 `invalid_line` message)."
          },
          "category_id": {
            "type": "integer",
            "nullable": true,
            "description": "A spend category of the business; an archived one is refused."
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true
          },
          "vendor": {
            "type": "string",
            "maxLength": 500,
            "nullable": true
          },
          "spent_on": {
            "type": "string",
            "format": "date",
            "description": "Required. For an expense line, not more than a day in the future."
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99999999999999,
            "description": "Required above zero for an expense line."
          },
          "tax_cents": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99999999999999
          },
          "miles": {
            "type": "number",
            "minimum": 0,
            "description": "Required above zero for a mileage line."
          },
          "days": {
            "type": "number",
            "description": "Required above zero for a per-diem line (at most 9999.99)."
          },
          "location_code": {
            "type": "string",
            "description": "Required for a per-diem line."
          },
          "cost_center": {
            "type": "string",
            "maxLength": 500,
            "nullable": true
          },
          "project_code": {
            "type": "string",
            "maxLength": 500,
            "nullable": true
          },
          "client_reference": {
            "type": "string",
            "maxLength": 500,
            "nullable": true
          },
          "billable_to_client": {
            "type": "boolean",
            "description": "A null or blank value is treated as not sent."
          },
          "payment_method": {
            "type": "string",
            "nullable": true,
            "description": "How it was paid (for example cash, personal_card, corporate_card or other)."
          }
        }
      },
      "MangoSpendPolicyViolation": {
        "type": "object",
        "description": "A spend-policy finding on a report or one of its lines.",
        "properties": {
          "line_id": {
            "type": "integer",
            "nullable": true
          },
          "code": {
            "type": "string",
            "description": "Stable identifier, for example `amount_missing` or `missing_receipt`."
          },
          "severity": {
            "type": "string",
            "enum": [
              "block",
              "warn"
            ],
            "description": "A `block` finding stops the report being submitted."
          },
          "message": {
            "type": "string"
          },
          "metadata": {
            "type": "object"
          }
        }
      },
      "MangoSpendPaginationMeta": {
        "type": "object",
        "description": "The API's standard pagination object, as `build_pagination_meta` answers it (the shared PaginationMeta schema lacks the two `has_*` flags this endpoint sends).",
        "required": [
          "total_count",
          "total_pages",
          "current_page",
          "per_page",
          "has_next_page",
          "has_prev_page"
        ],
        "properties": {
          "total_count": {
            "type": "integer",
            "description": "Rows in the whole filtered set."
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "has_next_page": {
            "type": "boolean"
          },
          "has_prev_page": {
            "type": "boolean"
          }
        }
      },
      "MangoSpendSummary": {
        "type": "object",
        "description": "The caller's roll-up. The first block is always present; the approver and company blocks appear only for a caller who holds that role AND a token with the wide `read:mango_spend` grant (or a session). Money is in minor units; the `*_totals` objects hold cents per currency code (zero currencies left out) and are the figure to show a multi-currency business — the single `*_cents` sums add currencies together.",
        "required": [
          "currency",
          "drafts",
          "in_flight",
          "paid_ytd_cents",
          "changes_requested",
          "returned_for_changes",
          "to_finish",
          "in_flight_totals",
          "awaiting_payment_totals",
          "paid_ytd_totals"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "description": "The business's default currency."
          },
          "drafts": {
            "type": "integer",
            "description": "The caller's draft reports."
          },
          "in_flight": {
            "type": "integer",
            "description": "The caller's reports on their way to being paid: submitted (awaiting a decision) plus approved (decided, not yet paid)."
          },
          "paid_ytd_cents": {
            "type": "integer",
            "description": "The reimbursable cents of the caller's own reports paid since January 1, summed across currencies into one figure (see `paid_ytd_totals` for the per-currency split)."
          },
          "changes_requested": {
            "type": "integer",
            "description": "Claims an approver returned for changes."
          },
          "returned_for_changes": {
            "type": "integer",
            "description": "The same count as `changes_requested`."
          },
          "to_finish": {
            "type": "integer",
            "description": "Drafts plus returned claims — the caller's to finish."
          },
          "in_flight_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "awaiting_payment_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "paid_ytd_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "awaiting_approval": {
            "type": "integer",
            "description": "Approver only — reports in the caller's approval queue (what `GET /mango_spend/approvals` lists)."
          },
          "awaiting_approval_cents": {
            "type": "integer",
            "description": "Approver only."
          },
          "awaiting_approval_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Approver only."
          },
          "unpaid_approved_count": {
            "type": "integer",
            "description": "Callers who may view all spend only — approved reports in no payment batch."
          },
          "unpaid_approved_cents": {
            "type": "integer",
            "description": "Company block only."
          },
          "unpaid_approved_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Company block only."
          },
          "owed_approved_count": {
            "type": "integer",
            "description": "Company block only — every approved report not yet paid, in a batch or not."
          },
          "owed_approved_cents": {
            "type": "integer",
            "description": "Company block only."
          },
          "owed_approved_totals": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Company block only."
          }
        }
      },
      "MealComplianceBreakComplianceTile": {
        "type": "object",
        "required": [
          "key",
          "label",
          "value",
          "caption",
          "icon",
          "tone"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "findings",
              "unresolved",
              "employees_affected",
              "by_period"
            ]
          },
          "label": {
            "type": "string"
          },
          "value": {
            "type": "integer"
          },
          "caption": {
            "type": "string"
          },
          "hint": {
            "type": "string",
            "description": "by_period only — the explanation of Unbroken work."
          },
          "icon": {
            "type": "string",
            "description": "A Font Awesome icon name."
          },
          "tone": {
            "type": "string",
            "enum": [
              "primary",
              "danger",
              "info",
              "warning"
            ]
          },
          "filter": {
            "type": "object",
            "nullable": true,
            "description": "unresolved only — the query that opens what it counted; null at zero.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "open"
                ]
              }
            }
          },
          "parts": {
            "type": "array",
            "description": "by_period only — Meal, Rest and Unbroken work, which add up to Findings.",
            "items": {
              "type": "object",
              "properties": {
                "period_kind": {
                  "type": "string",
                  "enum": [
                    "meal",
                    "rest",
                    "any"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "selected": {
                  "type": "boolean"
                },
                "filter": {
                  "type": "object",
                  "nullable": true,
                  "description": "The query that opens this part's findings; null at zero.",
                  "properties": {
                    "period_kind": {
                      "type": "string",
                      "enum": [
                        "meal",
                        "rest",
                        "any"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "MealComplianceBreakComplianceReport": {
        "type": "object",
        "properties": {
          "applied_filters": {
            "type": "object",
            "description": "The filters IN FORCE (an unrecognised value is not one).",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "all",
                  "open",
                  "resolved"
                ]
              },
              "event_type": {
                "type": "string",
                "nullable": true
              },
              "period_kind": {
                "type": "string",
                "nullable": true
              },
              "from": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "to": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "employee": {
                "type": "object",
                "nullable": true,
                "description": "The employee the list is narrowed to — draw \"Showing findings for <name> only\".",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "employee_unknown": {
                "type": "boolean",
                "description": "A user_id that names nobody in this organization."
              },
              "active": {
                "type": "boolean",
                "description": "Whether any filter narrows the report."
              }
            }
          },
          "enforcement": {
            "type": "object",
            "description": "What the organization's break enforcement does to people today.",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "observe",
                  "enforce",
                  "off"
                ]
              },
              "label": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "icon": {
                "type": "string"
              },
              "tone": {
                "type": "string",
                "enum": [
                  "info",
                  "warning"
                ]
              },
              "can_change": {
                "type": "boolean",
                "description": "A business admin",
                "who may change it on the web (Work Policy Settings).": null
              }
            }
          },
          "tiles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MealComplianceBreakComplianceTile"
            }
          },
          "rules_in_force": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "listed",
                  "none",
                  "unavailable"
                ]
              },
              "total": {
                "type": "integer",
                "nullable": true,
                "description": "Every rule in force; items holds at most 25."
              },
              "note": {
                "type": "string",
                "nullable": true,
                "description": "What none or unavailable means."
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "rule_id": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "string"
                    },
                    "location_count": {
                      "type": "integer"
                    },
                    "locations": {
                      "type": "array",
                      "description": "At most 8; more_locations_count says how many more.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "more_locations_count": {
                      "type": "integer"
                    }
                  }
                }
              },
              "can_manage_rules": {
                "type": "boolean",
                "description": "A business admin",
                "who manages Labor Law Rules on the web.": null
              }
            }
          },
          "filters": {
            "type": "array",
            "description": "The status chips, counted under every other filter.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "all",
                    "open",
                    "resolved"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "selected": {
                  "type": "boolean"
                }
              }
            }
          },
          "options": {
            "type": "object",
            "description": "The filter sheet's choices, in the report's words.",
            "properties": {
              "event_types": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    }
                  }
                }
              },
              "period_kinds": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "items": {
            "type": "array",
            "description": "One page of findings, newest work date first.",
            "items": {
              "$ref": "#/components/schemas/MealComplianceBreakFinding"
            }
          },
          "empty_state": {
            "type": "object",
            "nullable": true,
            "description": "Present when no finding matches.",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "rules_unavailable",
                  "no_rules",
                  "filtered",
                  "none_recorded"
                ]
              },
              "title": {
                "type": "string"
              },
              "body": {
                "type": "string"
              },
              "can_clear_filters": {
                "type": "boolean"
              }
            }
          },
          "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"
              },
              "filter_ignored": {
                "type": "boolean",
                "description": "Present when a filter value was not recognised."
              },
              "ignored_filters": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "status",
                    "event_type",
                    "period_kind",
                    "from",
                    "to"
                  ]
                }
              },
              "filter_note": {
                "type": "string"
              }
            }
          },
          "permissions": {
            "type": "object",
            "properties": {
              "can_resolve": {
                "type": "boolean",
                "description": "The token may Resolve a finding (POST /meal_compliance/break_compliance/{id}/resolve)."
              },
              "can_change_enforcement_mode": {
                "type": "boolean"
              },
              "can_manage_rules": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "MealComplianceBreakFinding": {
        "type": "object",
        "description": "One break-law finding, as the Break Compliance report lists it (GET /meal_compliance/break_compliance `items[]`) and as Resolve answers with it.",
        "required": [
          "id",
          "work_date",
          "employee",
          "finding",
          "period",
          "status",
          "status_label"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "work_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "employee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "initials": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "finding": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "too_early",
                  "too_late",
                  "missing",
                  "short",
                  "second_meal_missing",
                  "unbroken_interval_exceeded"
                ]
              },
              "label": {
                "type": "string",
                "description": "e.g. \"Missing\", \"Started too late\", \"Unbroken stretch\""
              },
              "tone": {
                "type": "string",
                "enum": [
                  "warning",
                  "danger"
                ]
              }
            }
          },
          "period": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "meal",
                  "rest",
                  "any"
                ]
              },
              "ordinal": {
                "type": "integer"
              },
              "label": {
                "type": "string",
                "description": "e.g. \"1st meal period\", \"Rest break\", \"Unbroken work\""
              }
            }
          },
          "description": {
            "type": "string",
            "description": "The finding as a sentence."
          },
          "detail": {
            "type": "string",
            "description": "The finding's own words with its times and counts (and whether the employee reported it); the description when it has none."
          },
          "severity": {
            "type": "string",
            "enum": [
              "violation",
              "warning"
            ]
          },
          "mode": {
            "type": "object",
            "description": "The enforcement mode in force when the finding was detected.",
            "properties": {
              "key": {
                "type": "string",
                "enum": [
                  "observe",
                  "enforce"
                ]
              },
              "label": {
                "type": "string"
              },
              "tone": {
                "type": "string",
                "enum": [
                  "neutral",
                  "dark"
                ]
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "resolved"
            ]
          },
          "status_label": {
            "type": "string",
            "description": "\"Unresolved\" or \"Resolved\""
          },
          "resolution": {
            "type": "object",
            "nullable": true,
            "description": "Present once resolved.",
            "properties": {
              "resolved_at": {
                "type": "string",
                "format": "date-time"
              },
              "resolved_by": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "note": {
                "type": "string",
                "nullable": true,
                "description": "The resolution note",
                "when one was given.": null
              }
            }
          },
          "attendance_record_id": {
            "type": "integer",
            "nullable": true
          },
          "detected_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MealComplianceDashboardPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "initials": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "description": "Absolute URL; a generated initials tile when the person has no photo."
          }
        }
      },
      "MealComplianceDashboard": {
        "type": "object",
        "required": [
          "scope",
          "tiles",
          "withdrawals",
          "recent_activity",
          "behavior_changes",
          "permissions"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "team",
              "self"
            ],
            "description": "Whose records every figure covers — the whole organization, the caller's direct reports and themself, or the caller alone."
          },
          "tiles": {
            "type": "array",
            "description": "Always the four tiles, in this order: active_waivers, pending_signatures, behavior_alerts, invalidated. Labels and captions are the web Dashboard's words.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label",
                "value",
                "caption",
                "icon",
                "tone"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "active_waivers",
                    "pending_signatures",
                    "behavior_alerts",
                    "invalidated"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "value": {
                  "type": "integer"
                },
                "caption": {
                  "type": "string"
                },
                "icon": {
                  "type": "string",
                  "description": "A Font Awesome icon name (file-signature, clock, bell, triangle-exclamation)."
                },
                "tone": {
                  "type": "string",
                  "enum": [
                    "success",
                    "warning",
                    "info",
                    "danger"
                  ]
                },
                "coverage": {
                  "type": "object",
                  "description": "active_waivers only. Of the employees whose logged breaks show them waiving a meal in the last 30 days, how many have a waiver in force that waives a meal period.",
                  "properties": {
                    "percentage": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Null when nobody in view is waiving."
                    },
                    "waiving_employees": {
                      "type": "integer"
                    },
                    "covered": {
                      "type": "integer"
                    },
                    "not_covered": {
                      "type": "integer"
                    },
                    "not_covered_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "\"See N not covered\" when permissions.can_view_gaps (a link to the compliance-gaps report), \"N not covered\" otherwise; null when everyone is covered."
                    }
                  }
                },
                "awaiting_employee": {
                  "type": "integer",
                  "description": "pending_signatures only — out for signature",
                  "waiting on the employee.": null
                },
                "waiting_on_manager": {
                  "type": "integer",
                  "description": "pending_signatures only — never sent, its signing request or link lapsed, or its term ended: a manager has to act."
                },
                "window_days": {
                  "type": "integer",
                  "description": "behavior_alerts only — the window",
                  "in days.": null
                }
              }
            }
          },
          "withdrawals": {
            "type": "object",
            "description": "Employees in view who withdrew their waiver, or declined to sign one, in the last `window_days` days and for whom no new waiver has been started since. Never the caller's own. The 10 most recent, with the uncapped `total`.",
            "properties": {
              "total": {
                "type": "integer"
              },
              "window_days": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "waiver_id": {
                      "type": "integer"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "withdrawn",
                        "declined"
                      ]
                    },
                    "description": {
                      "type": "string"
                    },
                    "ended_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "employee": {
                      "$ref": "#/components/schemas/MealComplianceDashboardPerson"
                    }
                  }
                }
              }
            }
          },
          "recent_activity": {
            "type": "object",
            "description": "The most recently updated waivers, newest first.",
            "properties": {
              "limit": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "waiver_id": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "draft",
                        "pending_signature",
                        "active",
                        "not_yet_in_effect",
                        "expired",
                        "invalidated"
                      ],
                      "description": "The status as the waiver's badge reads it (in force today, not the stored column)."
                    },
                    "status_label": {
                      "type": "string",
                      "description": "The badge's words — e.g. \"Pending — needs sending\", \"Withdrawn\", \"Declined\", \"Not yet in effect\"."
                    },
                    "tone": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "success",
                        "warning",
                        "danger",
                        "neutral",
                        "info",
                        "primary"
                      ]
                    },
                    "location": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "employee": {
                      "$ref": "#/components/schemas/MealComplianceDashboardPerson"
                    }
                  }
                }
              }
            }
          },
          "behavior_changes": {
            "type": "object",
            "description": "Detected changes in break behavior in the last `window_days` days, newest first: the 10 most recent, with the uncapped `total` (the Behavior alerts tile's value).",
            "properties": {
              "total": {
                "type": "integer"
              },
              "window_days": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "previous_pattern": {
                      "type": "string",
                      "enum": [
                        "waiving",
                        "taking",
                        "inconsistent"
                      ]
                    },
                    "previous_pattern_label": {
                      "type": "string"
                    },
                    "detected_pattern": {
                      "type": "string",
                      "enum": [
                        "waiving",
                        "taking",
                        "inconsistent"
                      ]
                    },
                    "detected_pattern_label": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "string",
                      "example": "Changed from Taking to Waiving"
                    },
                    "detected_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "employee": {
                      "$ref": "#/components/schemas/MealComplianceDashboardPerson"
                    }
                  }
                }
              }
            }
          },
          "permissions": {
            "type": "object",
            "description": "What the caller's role and token allow from this screen.",
            "properties": {
              "can_create_waiver": {
                "type": "boolean",
                "description": "The + (New Waiver) button."
              },
              "can_view_waivers": {
                "type": "boolean",
                "description": "View All — the waiver register."
              },
              "can_view_gaps": {
                "type": "boolean",
                "description": "The not-covered link to the compliance-gaps report."
              },
              "gaps_web_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Where \"See N not covered\" opens: the web compliance-gaps report (it has no API endpoint), for the caller signed in on the web. Present exactly when `can_view_gaps`; null otherwise."
              }
            }
          }
        }
      },
      "MealComplianceMyBreaksWaiver": {
        "type": "object",
        "description": "The fields every waiver in this response carries.",
        "required": [
          "id",
          "status",
          "status_label",
          "behavior_state",
          "elections",
          "details_path"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending_signature",
              "active",
              "not_yet_in_effect",
              "expired",
              "invalidated"
            ],
            "description": "The status as the waiver's badge reads it (in force today, not the stored column)."
          },
          "status_label": {
            "type": "string",
            "description": "The badge's words — e.g. \"Active\", \"Not yet in effect\", \"Pending — link expired\", \"Withdrawn\", \"Declined\"."
          },
          "tone": {
            "type": "string",
            "nullable": true,
            "enum": [
              "success",
              "warning",
              "danger",
              "neutral",
              "info",
              "primary"
            ],
            "description": "The badge's colour, the same tone the web register uses."
          },
          "behavior_state": {
            "type": "string",
            "enum": [
              "waiving",
              "taking"
            ]
          },
          "elections": {
            "type": "array",
            "description": "Per meal period when the agreement records them; empty for a single election.",
            "items": {
              "type": "object",
              "properties": {
                "period_ordinal": {
                  "type": "integer"
                },
                "label": {
                  "type": "string",
                  "example": "1st meal period"
                },
                "waived": {
                  "type": "boolean"
                },
                "choice_label": {
                  "type": "string",
                  "example": "Waived"
                }
              }
            }
          },
          "elections_summary": {
            "type": "string",
            "description": "The choice in one line — the per-period elections, else \"Waiving meal breaks\" / \"Taking meal breaks\"."
          },
          "jurisdiction_name": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "valid_from": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "valid_until": {
            "type": "object",
            "description": "The end of the term, in the Waivers register's shape.",
            "properties": {
              "date": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "Null means no end date."
              },
              "label": {
                "type": "string",
                "nullable": true,
                "description": "\"No expiry\" when there is no end date."
              },
              "days_until_expiry": {
                "type": "integer",
                "nullable": true,
                "description": "Negative once the term has passed."
              },
              "tag": {
                "type": "object",
                "nullable": true,
                "description": "The register's tag beside the date — `expiring` when in force and inside the expiry-reminder window, `expired` for a draft or pending waiver past its term.",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "expiring",
                      "expired"
                    ]
                  },
                  "label": {
                    "type": "string"
                  },
                  "tone": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "The manager's note",
            "printed on the employee's copy.": null
          },
          "is_sample_data": {
            "type": "boolean"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "signed_copy": {
            "type": "object",
            "nullable": true,
            "description": "The caller's own signed copy, only where E-Signature would let them download it.",
            "properties": {
              "esignature_request_id": {
                "type": "integer",
                "description": "GET /esignature/{id} answers a time-limited signed_document_url."
              },
              "download_path": {
                "type": "string",
                "description": "The web download path (session-authenticated)."
              }
            }
          },
          "details_path": {
            "type": "string",
            "description": "The web waiver page",
            "which admits its own employee.": null
          }
        }
      },
      "MealComplianceMyBreaks": {
        "type": "object",
        "required": [
          "title",
          "intro",
          "pending_count",
          "break_pattern",
          "waiver_history",
          "drafts"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "intro": {
            "type": "string"
          },
          "pending_waiver": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/MealComplianceMyBreaksWaiver"
              },
              {
                "type": "object",
                "properties": {
                  "heading": {
                    "type": "string"
                  },
                  "terms": {
                    "type": "string",
                    "nullable": true,
                    "description": "What signing records. Null once signing is closed for the caller."
                  },
                  "signing": {
                    "type": "object",
                    "properties": {
                      "state": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                          "ready",
                          "never_sent",
                          "request_voided",
                          "request_declined",
                          "request_expired",
                          "signing_complete",
                          "not_a_signer",
                          "you_signed",
                          "signed_copy_failed",
                          "you_declined",
                          "link_expired",
                          "awaiting_other_signers",
                          "no_link",
                          "signing_switched_off"
                        ]
                      },
                      "can_sign": {
                        "type": "boolean",
                        "description": "The Review & Sign button."
                      },
                      "sign_url": {
                        "type": "string",
                        "nullable": true,
                        "description": "The caller's own live signing link, when can_sign."
                      },
                      "web_view_url": {
                        "type": "string",
                        "nullable": true,
                        "description": "The app's Review & Sign sheet, when can_sign: the waiver as the document they sign, read-only — the organization, title, rows, agreement and notes, the same content its PDF is made from — at `GET /m/apps/meal-compliance/{id}/document`, a signed-in /m/ page with no app chrome, for the app's web view. The app draws the signature pad, consent, Sign Document (`POST /meal_compliance/waivers/{id}/sign`) and Decline (`POST …/decline`)."
                      },
                      "can_sign_in_app": {
                        "type": "boolean",
                        "description": "The app's own Sign Document — false on a read-only token, under an admin's View-as, for a sample waiver, or once the waiver's term has ended."
                      },
                      "sign": {
                        "type": "object",
                        "nullable": true,
                        "description": "What the app's Sign step asks for, when can_sign_in_app.",
                        "properties": {
                          "label": {
                            "type": "string",
                            "example": "Sign Document"
                          },
                          "signature_label": {
                            "type": "string",
                            "example": "Signature"
                          },
                          "signature_types": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "typed",
                                "drawn"
                              ]
                            }
                          },
                          "signature_fonts": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "cursive",
                                "elegant",
                                "formal"
                              ]
                            }
                          },
                          "date_label": {
                            "type": "string",
                            "example": "Date"
                          },
                          "date": {
                            "type": "string",
                            "format": "date",
                            "description": "Their own today — the default the Sign call uses when it sends no date."
                          },
                          "consent_label": {
                            "type": "string",
                            "description": "The consent the signer confirms (consent true on Sign). It refers to the disclosure below, which the app shows above it.",
                            "example": "I have read the electronic records disclosure and agree to sign electronically. My electronic signature is legally binding."
                          },
                          "disclosure": {
                            "type": "object",
                            "description": "The ESIGN electronic records disclosure the signer is shown before consenting — the same wording as the emailed signing page. Its version is recorded on the signed audit row.",
                            "properties": {
                              "title": {
                                "type": "string"
                              },
                              "text": {
                                "type": "string",
                                "description": "The paragraphs joined by blank lines."
                              },
                              "paragraphs": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "version": {
                                "type": "string",
                                "description": "The disclosure version recorded as esign_disclosure_version."
                              }
                            }
                          },
                          "hint": {
                            "type": "string"
                          }
                        }
                      },
                      "note": {
                        "type": "string",
                        "nullable": true,
                        "description": "Why the caller cannot sign now; null when they can."
                      },
                      "link_expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      }
                    }
                  },
                  "can_decline": {
                    "type": "boolean",
                    "description": "Decline to Sign (POST /meal_compliance/waivers/{id}/decline)."
                  },
                  "decline": {
                    "type": "object",
                    "nullable": true,
                    "description": "The \"Decline to Sign\" sheet, when can_decline.",
                    "properties": {
                      "label": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      },
                      "message": {
                        "type": "string"
                      },
                      "confirm_label": {
                        "type": "string"
                      },
                      "reason_field": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "required": {
                            "type": "boolean"
                          },
                          "max_length": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  },
                  "can_withdraw": {
                    "type": "boolean",
                    "description": "Withdraw before signing — only where can_decline is false."
                  },
                  "withdraw_confirm": {
                    "type": "string",
                    "nullable": true
                  },
                  "sample_note": {
                    "type": "string",
                    "nullable": true,
                    "description": "Why a sample waiver offers no Withdraw."
                  }
                }
              }
            ]
          },
          "pending_count": {
            "type": "integer",
            "description": "How many of the caller's waivers await signature."
          },
          "current_waiver": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/MealComplianceMyBreaksWaiver"
              },
              {
                "type": "object",
                "properties": {
                  "choice_label": {
                    "type": "string",
                    "example": "Waiving meal breaks"
                  },
                  "in_force": {
                    "type": "boolean",
                    "description": "In force today (not starting later",
                    "not past its term).": null
                  },
                  "start_sentence": {
                    "type": "string",
                    "nullable": true
                  },
                  "end_sentence": {
                    "type": "string"
                  },
                  "expiry": {
                    "type": "object",
                    "properties": {
                      "expiring_soon": {
                        "type": "boolean",
                        "description": "In force and inside the organization's expiry-reminder window."
                      },
                      "label": {
                        "type": "string",
                        "nullable": true,
                        "example": "Expires in 9 days"
                      },
                      "renewed": {
                        "type": "boolean",
                        "description": "A signed renewal already runs past this one's end."
                      },
                      "renewed_note": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  },
                  "can_revoke": {
                    "type": "boolean",
                    "description": "Revoke my waiver."
                  },
                  "revoke_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "\"Revoke my waiver\"; \"Withdraw this waiver\" for one whose start date is still ahead; \"Withdraw my election\" for an agreement that waives no meal period."
                  },
                  "revoke_explainer": {
                    "type": "string",
                    "nullable": true
                  },
                  "revoke_confirm": {
                    "type": "string",
                    "nullable": true
                  },
                  "sample_note": {
                    "type": "string",
                    "nullable": true,
                    "description": "Why a sample waiver offers no Revoke."
                  }
                }
              }
            ]
          },
          "no_current_waiver": {
            "type": "object",
            "nullable": true,
            "description": "The empty state, present only when current_waiver is null.",
            "properties": {
              "title": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "break_pattern": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Every detected observation on file (uncapped)."
              },
              "limit": {
                "type": "integer",
                "description": "How many the list holds (10)."
              },
              "truncation_note": {
                "type": "string",
                "nullable": true
              },
              "empty_state": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "label": {
                      "type": "string",
                      "description": "The window and its verdict, e.g. \"Oct 1 – Oct 15: skipped 11 of 14 days\"."
                    },
                    "detected_pattern": {
                      "type": "string",
                      "enum": [
                        "waiving",
                        "taking",
                        "inconsistent"
                      ]
                    },
                    "previous_pattern": {
                      "type": "string",
                      "enum": [
                        "waiving",
                        "taking",
                        "inconsistent",
                        "unknown"
                      ]
                    },
                    "window_start": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    },
                    "window_end": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    },
                    "detected_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "days_analyzed": {
                      "type": "integer",
                      "nullable": true
                    },
                    "days_waived": {
                      "type": "integer",
                      "nullable": true
                    },
                    "days_taken": {
                      "type": "integer",
                      "nullable": true
                    },
                    "action_label": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "waiver_history": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/MealComplianceMyBreaksWaiver"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "choice_label": {
                          "type": "string",
                          "example": "Waiving"
                        },
                        "valid_label": {
                          "type": "string",
                          "example": "Sep 2025 – Sep 2026"
                        },
                        "updated_on": {
                          "type": "string",
                          "example": "Sep 16, 2026"
                        },
                        "ended_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Why an invalidated waiver ended, said to its own employee."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "drafts": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              },
              "note": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "MealComplianceWaiverEmployeeOption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "Only where the People directory shows this caller email."
          },
          "initials": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string"
          },
          "job_title": {
            "type": "string",
            "nullable": true,
            "description": "Only where the People directory shows this caller job titles."
          },
          "meta": {
            "type": "string",
            "nullable": true,
            "description": "The secondary line that tells two same-named people apart."
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "object",
            "nullable": true,
            "description": "The Location the form suggests — their primary location here, when active.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "jurisdiction": {
            "type": "object",
            "nullable": true,
            "description": "The Jurisdiction the form suggests — their location's state one, else city, else country.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "label": {
                "type": "string",
                "example": "California (CA)"
              },
              "level": {
                "type": "string",
                "enum": [
                  "country",
                  "state",
                  "city"
                ]
              }
            }
          },
          "in_progress_waiver": {
            "type": "object",
            "nullable": true,
            "description": "A waiver already on its way for them — saving another needs confirming.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "status": {
                "type": "string"
              },
              "status_label": {
                "type": "string"
              }
            }
          }
        }
      },
      "MealComplianceNewWaiverForm": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "renewal_of": {
            "type": "integer",
            "nullable": true,
            "description": "The waiver `from_waiver_id` renewed."
          },
          "employee": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/MealComplianceWaiverEmployeeOption"
              }
            ]
          },
          "in_progress_waiver": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "status": {
                "type": "string"
              },
              "status_label": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "create_override": {
            "type": "object",
            "nullable": true,
            "description": "How to save a second waiver beside the one on its way — send `param` = `value` with the create.",
            "properties": {
              "param": {
                "type": "string",
                "example": "create_beside_in_progress"
              },
              "value": {
                "type": "integer"
              },
              "label": {
                "type": "string",
                "example": "Create another waiver anyway"
              }
            }
          },
          "fields": {
            "type": "object",
            "description": "Each field's label, value (the default or the prefill), placeholder, choices and help.",
            "properties": {
              "employee": {
                "type": "object",
                "description": "label, required, value, placeholder — and `scope_note`, the line under the picker for a line manager (only their direct reports are listed), null for HR and administrators."
              },
              "location": {
                "type": "object"
              },
              "behavior_state": {
                "type": "object",
                "description": "`shown: false` where per-period elections decide it."
              },
              "elections": {
                "type": "object",
                "properties": {
                  "shown": {
                    "type": "boolean"
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "periods": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "ordinal": {
                          "type": "integer"
                        },
                        "label": {
                          "type": "string"
                        },
                        "waived": {
                          "type": "boolean"
                        },
                        "note": {
                          "type": "string",
                          "nullable": true,
                          "description": "\"Waivable only on days over 10 and up to 12 hours worked.\""
                        }
                      }
                    }
                  }
                }
              },
              "jurisdiction": {
                "type": "object"
              },
              "valid_from": {
                "type": "object"
              },
              "valid_until": {
                "type": "object"
              },
              "notes": {
                "type": "object"
              }
            }
          },
          "submit_label": {
            "type": "string"
          }
        }
      },
      "MealComplianceWaiverCreateRequest": {
        "type": "object",
        "required": [
          "user_id"
        ],
        "properties": {
          "user_id": {
            "type": "integer",
            "description": "An active member the caller may create a waiver for (a line manager: a direct report)."
          },
          "behavior_state": {
            "type": "string",
            "enum": [
              "waiving",
              "taking"
            ],
            "description": "Required where it is chosen (422 when absent or not one of these two). Where per-period elections decide it (`fields.elections.shown` in GET …/new) it is ignored and derived — waiving when any period is waived."
          },
          "elections": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "Where per-period elections decide Behavior State: per meal period, keyed by ordinal (`{\"1\": false, \"2\": true}`). Every period the organization defines is recorded — one left out is taking; ordinals it does not define are ignored. Not an object → 422. Ignored where Behavior State is chosen.",
            "example": {
              "1": false,
              "2": true
            }
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "One of the form's active locations (422 otherwise)."
          },
          "jurisdiction_id": {
            "type": "integer",
            "nullable": true,
            "description": "One of the form's active jurisdictions (422 otherwise)."
          },
          "valid_from": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Absent or null — none; activating the waiver then starts it on the day it is activated."
          },
          "valid_until": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Today or later. Absent or null — no expiration (the form's default term is in `GET …/waivers/new`)."
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000,
            "description": "The employee sees this — it is printed on the signed waiver."
          },
          "create_beside_in_progress": {
            "type": "integer",
            "description": "Confirms saving beside the waiver already on its way — its id, from a 409's `create_override.value`."
          }
        }
      },
      "MealComplianceWaiverActionError": {
        "type": "object",
        "description": "A refused or failed action, in the words the web page uses. `error.details.page` is the waiver page as it is now (the body of `GET /meal_compliance/waivers/{id}`), so a client that pressed a stale button redraws the truth.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "esignature_off",
                  "esignature_unavailable",
                  "not_sendable",
                  "term_ended",
                  "former_member",
                  "no_email",
                  "signing_portal_closed",
                  "send_failed",
                  "not_activatable",
                  "invalid",
                  "not_invalidatable",
                  "invalidate_failed",
                  "state_changed",
                  "not_resendable",
                  "resend_failed",
                  "not_revocable",
                  "revoke_failed",
                  "not_declinable",
                  "reason_too_long",
                  "decline_failed",
                  "forbidden",
                  "own_waiver",
                  "not_signable",
                  "consent_required",
                  "signature_invalid",
                  "date_invalid",
                  "signature_too_large",
                  "fields_missing",
                  "sign_failed"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "properties": {
                  "page": {
                    "$ref": "#/components/schemas/MealComplianceWaiverDetails"
                  }
                }
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "not_activatable",
            "message": "Only a draft waiver, or a pending one with no live signing link, can be activated — this one is active.",
            "details": {
              "page": {
                "waiver": {
                  "id": 904,
                  "status": "active"
                },
                "permissions": {
                  "can_activate": false
                }
              }
            }
          }
        }
      },
      "MealComplianceWaiverDetails": {
        "type": "object",
        "properties": {
          "waiver": {
            "type": "object",
            "description": "The hero and Waiver Information. Optional rows are null when they do not apply.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "pending_signature",
                  "active",
                  "invalidated",
                  "expired",
                  "not_yet_in_effect"
                ],
                "description": "The status as the badge reads it."
              },
              "status_label": {
                "type": "string",
                "description": "The badge's words: \"Pending — needs sending\", \"Pending — link expired\", \"Withdrawn\", \"Declined\", \"Discarded\", \"Not yet in effect\", …"
              },
              "tone": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "primary",
                  "success",
                  "warning",
                  "danger",
                  "neutral",
                  "info",
                  null
                ]
              },
              "employee": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "nullable": true,
                    "description": "Where the People directory shows this caller email, or on the caller's own waiver."
                  },
                  "initials": {
                    "type": "string"
                  },
                  "avatar_url": {
                    "type": "string",
                    "description": "Absolute URL; a generated initials tile when the person has no photo."
                  },
                  "job_title": {
                    "type": "string",
                    "nullable": true,
                    "description": "Their job title in this organization (the hero line)."
                  },
                  "active_member": {
                    "type": "boolean",
                    "description": "False for a former employee (\"No longer a member\")."
                  }
                }
              },
              "behavior": {
                "type": "object",
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "waiving",
                      "taking"
                    ]
                  },
                  "label": {
                    "type": "string"
                  },
                  "icon": {
                    "type": "string",
                    "description": "A Font Awesome icon name (forward, utensils)."
                  }
                }
              },
              "elections": {
                "type": "array",
                "description": "Per meal period, when the waiver records them; empty for a single election.",
                "items": {
                  "type": "object",
                  "properties": {
                    "period_ordinal": {
                      "type": "integer"
                    },
                    "label": {
                      "type": "string"
                    },
                    "waived": {
                      "type": "boolean"
                    },
                    "choice_label": {
                      "type": "string"
                    }
                  }
                }
              },
              "elections_summary": {
                "type": "string",
                "nullable": true,
                "description": "The \"Meal period elections\" row; null when the waiver records none."
              },
              "jurisdiction_name": {
                "type": "string",
                "nullable": true,
                "description": "Null when none is set; the page prints \"Not specified\"."
              },
              "location": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "valid_from": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "Null prints \"Not set\"."
              },
              "valid_from_tag": {
                "type": "object",
                "nullable": true,
                "description": "\"Starts in N days\" beside a Valid From still ahead.",
                "properties": {
                  "days": {
                    "type": "integer"
                  },
                  "label": {
                    "type": "string"
                  },
                  "tone": {
                    "type": "string",
                    "enum": [
                      "info"
                    ]
                  }
                }
              },
              "valid_until": {
                "type": "object",
                "description": "The Waivers register's shape, plus `note`.",
                "properties": {
                  "date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "label": {
                    "type": "string",
                    "nullable": true,
                    "description": "\"No expiry\" when the waiver has no end date."
                  },
                  "days_until_expiry": {
                    "type": "integer",
                    "nullable": true
                  },
                  "tag": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "expiring",
                          "expired"
                        ]
                      },
                      "label": {
                        "type": "string"
                      },
                      "tone": {
                        "type": "string",
                        "enum": [
                          "warning",
                          "danger"
                        ]
                      }
                    }
                  },
                  "note": {
                    "type": "string",
                    "nullable": true,
                    "description": "Why Activate is gone on a draft or pending waiver whose term already ended."
                  }
                }
              },
              "ended": {
                "type": "object",
                "nullable": true,
                "description": "How an invalidated waiver ended; null otherwise.",
                "properties": {
                  "label": {
                    "type": "string",
                    "description": "Withdrawn / Declined / Discarded / Invalidated."
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "reason": {
                    "type": "string",
                    "description": "Who ended it and why, in words (to the employee on their own waiver)."
                  },
                  "by": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  "by_line": {
                    "type": "string",
                    "description": "\"By Dana Lee.\" or \"Acting user not recorded.\""
                  }
                }
              },
              "activated_by": {
                "type": "object",
                "nullable": true,
                "description": "Who put it on file without an in-app signature.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              },
              "notes": {
                "type": "string",
                "nullable": true
              },
              "is_sample_data": {
                "type": "boolean"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "esignature": {
            "type": "object",
            "nullable": true,
            "description": "E-Signature Status, once a request exists.",
            "properties": {
              "request_id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "sent",
                  "viewed",
                  "partially_signed",
                  "completed",
                  "declined",
                  "voided",
                  "expired"
                ]
              },
              "status_label": {
                "type": "string"
              },
              "decline_reason": {
                "type": "string",
                "nullable": true,
                "description": "The reason the signer gave when they declined (\"Reason given: “…”\") — for the signer, anyone who may act on this employee's waivers (HR, a Meal Compliance app admin, their line manager), and anyone E-Signature lets read it."
              },
              "sent_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "signing_link": {
                "type": "object",
                "nullable": true,
                "description": "While the request is out — when the employee's link stops working.",
                "properties": {
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "expired": {
                    "type": "boolean"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Signing link valid until\" / \"Signing link expired\"."
                  }
                }
              },
              "signed_copy": {
                "type": "object",
                "nullable": true,
                "description": "The signed copy, only where E-Signature would let this caller open it. `GET /esignature/{esignature_request_id}` answers a time-limited `signed_document_url`.",
                "properties": {
                  "esignature_request_id": {
                    "type": "integer"
                  },
                  "download_path": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "behavior_history": {
            "type": "object",
            "description": "The employee's detected break-pattern windows, newest first, at most `limit`.",
            "properties": {
              "limit": {
                "type": "integer",
                "example": 20
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "label": {
                      "type": "string",
                      "description": "\"Sep 19 – Oct 3: skipped 13 of 14 days\"."
                    },
                    "detected_pattern": {
                      "type": "string"
                    },
                    "previous_pattern": {
                      "type": "string",
                      "nullable": true
                    },
                    "window_start": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    },
                    "window_end": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    },
                    "detected_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "days_analyzed": {
                      "type": "integer",
                      "nullable": true
                    },
                    "days_waived": {
                      "type": "integer",
                      "nullable": true
                    },
                    "days_taken": {
                      "type": "integer",
                      "nullable": true
                    },
                    "action_label": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "notices": {
            "type": "array",
            "description": "Why an action the page would offer is missing.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "former_member",
                    "esignature_app_off",
                    "no_email",
                    "signing_portal_closed",
                    "resend_former_member"
                  ]
                },
                "message": {
                  "type": "string"
                },
                "icon": {
                  "type": "string"
                }
              }
            }
          },
          "permissions": {
            "type": "object",
            "description": "One flag per action — true only when this caller may take it on this waiver now.",
            "properties": {
              "can_send_for_signature": {
                "type": "boolean"
              },
              "can_activate": {
                "type": "boolean",
                "description": "Activate — never on the caller's own waiver."
              },
              "can_invalidate": {
                "type": "boolean",
                "description": "Invalidate — on the employer's initiative."
              },
              "can_record_employee_withdrawal": {
                "type": "boolean",
                "description": "Employee withdrew it (active) / Employee declined it (pending)."
              },
              "can_discard_draft": {
                "type": "boolean",
                "description": "Discard draft / Discard (never sent) — a draft, or a pending waiver whose signature request never went out."
              },
              "can_resend_signing_link": {
                "type": "boolean"
              },
              "can_download_signed_waiver": {
                "type": "boolean"
              },
              "can_start_new_waiver": {
                "type": "boolean",
                "description": "\"Start a new waiver for …\" on an ended waiver, \"Start the next waiver for …\" near expiry."
              },
              "can_revoke_own_waiver": {
                "type": "boolean",
                "description": "Revoke my waiver / Withdraw before signing — the waiver's own employee."
              },
              "can_decline_to_sign": {
                "type": "boolean",
                "description": "Decline to Sign — the waiver's own employee, where Review & Sign is offered (POST …/decline). Where it is true, can_revoke_own_waiver is false on a pending waiver."
              },
              "can_sign_in_app": {
                "type": "boolean",
                "description": "The app's own Sign Document on its Review & Sign sheet (POST …/sign) — the same live signing request Decline answers."
              },
              "can_review_and_sign": {
                "type": "boolean",
                "description": "Review & Sign — the waiver's own employee's live signing link (the review_and_sign action's sign_url and its read-only document web_view_url). Never under an admin's View-as or for a sample waiver. On a read-only token it stays true while can_sign_in_app is false: show the document and send them to sign_url, which needs no API scope."
              }
            }
          },
          "actions": {
            "type": "array",
            "description": "The permitted actions only, in the order and place the prototype draws them.",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "send_for_signature",
                    "activate",
                    "start_new_waiver",
                    "record_employee_withdrawal",
                    "invalidate",
                    "review_and_sign",
                    "decline_to_sign",
                    "revoke_own_waiver",
                    "discard_draft",
                    "resend_signing_link",
                    "download_signed_waiver"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "icon": {
                  "type": "string",
                  "description": "A Font Awesome icon name."
                },
                "tone": {
                  "type": "string",
                  "enum": [
                    "primary",
                    "success",
                    "danger"
                  ]
                },
                "placement": {
                  "type": "string",
                  "enum": [
                    "primary",
                    "pinned",
                    "menu"
                  ]
                },
                "confirm": {
                  "type": "object",
                  "nullable": true,
                  "description": "The confirm sheet; null for an action that asks nothing (a download, opening the New Waiver form).",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "confirm_label": {
                      "type": "string"
                    }
                  }
                },
                "reason": {
                  "type": "string",
                  "description": "Invalidate / withdrawal / discard: the invalidation source the action records (manual, employee_revoked)."
                },
                "discard": {
                  "type": "boolean",
                  "description": "discard_draft only: send `discard: true` with the press."
                },
                "sign_url": {
                  "type": "string",
                  "description": "review_and_sign only: the caller's own live signing link (the web signing page)."
                },
                "web_view_url": {
                  "type": "string",
                  "description": "review_and_sign only: the waiver as a read-only document for the app's web view (GET /m/apps/meal-compliance/{id}/document) — see My Breaks' signing.web_view_url."
                },
                "reason_field": {
                  "type": "object",
                  "description": "decline_to_sign: the optional reason the sheet asks for (sent as `reason`).",
                  "properties": {
                    "label": {
                      "type": "string",
                      "example": "Reason (optional)"
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "max_length": {
                      "type": "integer",
                      "example": 1000
                    }
                  }
                },
                "prefill": {
                  "type": "object",
                  "description": "start_new_waiver: what the New Waiver form opens with.",
                  "properties": {
                    "user_id": {
                      "type": "integer"
                    },
                    "from_waiver_id": {
                      "type": "integer"
                    }
                  }
                },
                "esignature_request_id": {
                  "type": "integer",
                  "description": "download_signed_waiver only."
                },
                "download_path": {
                  "type": "string",
                  "description": "download_signed_waiver only."
                }
              }
            }
          }
        }
      },
      "MealComplianceWaivers": {
        "type": "object",
        "required": [
          "scope",
          "status",
          "filters",
          "locations",
          "items",
          "meta",
          "permissions"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "team",
              "self"
            ],
            "description": "Whose waivers the list and the counts cover — the whole organization, the caller's direct reports and themself, or the caller alone."
          },
          "status": {
            "type": "string",
            "description": "The status chip the list is filtered to (`all` when none)."
          },
          "search": {
            "type": "string",
            "nullable": true,
            "description": "The search the list honours, trimmed."
          },
          "location_id": {
            "type": "string",
            "nullable": true
          },
          "sort": {
            "type": "string",
            "nullable": true,
            "enum": [
              "valid_until",
              "employee",
              null
            ]
          },
          "dir": {
            "type": "string",
            "nullable": true,
            "enum": [
              "asc",
              "desc",
              null
            ]
          },
          "filters": {
            "type": "array",
            "description": "The status chips in the register's order: all, active, (not_yet_in_effect while it holds a waiver or is selected), pending_signature, invalidated, expired, draft. The counts of the statuses add up to `all`.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label",
                "count",
                "selected"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "enum": [
                    "all",
                    "active",
                    "not_yet_in_effect",
                    "pending_signature",
                    "invalidated",
                    "expired",
                    "draft"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "selected": {
                  "type": "boolean"
                }
              }
            }
          },
          "locations": {
            "type": "array",
            "description": "The Location filter's options: the locations owning a waiver the caller may see, plus every active location for a whole-organization viewer. Draw the filter only when there is more than one.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MealComplianceWaiverCard"
            }
          },
          "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"
              },
              "filter_ignored": {
                "type": "boolean",
                "description": "Present when `status` was not recognised."
              },
              "filter_note": {
                "type": "string"
              }
            }
          },
          "permissions": {
            "type": "object",
            "properties": {
              "can_create_waiver": {
                "type": "boolean",
                "description": "The + (New Waiver): the caller may create waivers (full access, or a line manager) and the token holds `write:meal_compliance`."
              },
              "can_export": {
                "type": "boolean",
                "description": "The web register's Export — HR, business admins and Meal Compliance app admins."
              },
              "export_web_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Where Export opens: the web register's CSV download (it has no API endpoint), carrying this list's status, search, location and sort, for the caller signed in on the web. Present exactly when `can_export`; null otherwise."
              }
            }
          }
        }
      },
      "MealComplianceWaiverSearch": {
        "type": "object",
        "required": [
          "scope",
          "query",
          "searchable_by",
          "placeholder",
          "items",
          "meta"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "team",
              "self"
            ],
            "description": "Whose waivers were searched, as on the register."
          },
          "query": {
            "type": "string",
            "nullable": true,
            "description": "The query the results honour, trimmed; null on the idle screen."
          },
          "searchable_by": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "name",
                "email"
              ]
            },
            "description": "What the query matches. `email` only where the People directory shows this caller email — a search never confirms a hidden address."
          },
          "placeholder": {
            "type": "string",
            "description": "The search field's placeholder, naming what it searches."
          },
          "results_label": {
            "type": "string",
            "nullable": true,
            "description": "\"1 result\" / \"N results\" over every page; null when there is nothing to show."
          },
          "empty_state": {
            "type": "object",
            "nullable": true,
            "description": "The idle screen, or the no-results screen; null while there are results.",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "idle",
                  "no_results"
                ]
              },
              "title": {
                "type": "string"
              },
              "body": {
                "type": "string"
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/MealComplianceWaiverCard"
                },
                {
                  "type": "object",
                  "properties": {
                    "subtitle": {
                      "type": "string",
                      "description": "The line under the name: \"Meal Waiver (California) · North Warehouse\" — \"N/A\" for a waiver with no jurisdiction, no location part when it has none."
                    }
                  }
                }
              ]
            }
          },
          "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"
              }
            }
          }
        }
      },
      "MealComplianceWaiverCard": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending_signature",
              "active",
              "invalidated",
              "expired",
              "not_yet_in_effect"
            ],
            "description": "The status as the badge reads it — the key the chips count."
          },
          "status_label": {
            "type": "string",
            "description": "The badge's words, which can say more than the key: \"Pending — needs sending\", \"Pending — link expired\", \"Withdrawn\", \"Declined\", \"Not yet in effect\"."
          },
          "tone": {
            "type": "string",
            "nullable": true,
            "enum": [
              "primary",
              "success",
              "warning",
              "danger",
              "neutral",
              "info",
              null
            ],
            "description": "The badge colour, as on the web register and the Dashboard."
          },
          "employee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "description": "Only where the People directory shows this caller email."
              },
              "initials": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "description": "Absolute URL; a generated initials tile when the person has no photo."
              },
              "active_member": {
                "type": "boolean",
                "description": "False for a former employee — their waiver can no longer be sent or activated."
              }
            }
          },
          "behavior": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "waiving",
                  "taking"
                ]
              },
              "label": {
                "type": "string"
              },
              "icon": {
                "type": "string",
                "description": "A Font Awesome icon name (forward, utensils)."
              }
            }
          },
          "elections_summary": {
            "type": "string",
            "description": "\"1st meal period: Waived · 2nd meal period: Taking\" when the waiver records per-period elections, else \"Waiving meal breaks\" / \"Taking meal breaks\"."
          },
          "jurisdiction_name": {
            "type": "string",
            "nullable": true,
            "description": "Null when none is set; the web prints N/A."
          },
          "location": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "valid_from": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "valid_until": {
            "type": "object",
            "properties": {
              "date": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "label": {
                "type": "string",
                "nullable": true,
                "description": "\"No expiry\" when the waiver has no end date."
              },
              "days_until_expiry": {
                "type": "integer",
                "nullable": true,
                "description": "Negative once the date has passed."
              },
              "tag": {
                "type": "object",
                "nullable": true,
                "description": "The tag beside the date, as on the web register: `expiring` (warning) inside the organization's expiry-reminder window, `expired` (danger) for a draft or pending waiver past its term. Null otherwise — the status already says Expired, or the waiver was ended.",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "expiring",
                      "expired"
                    ]
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Expires in 3 days\", \"Expires today\", \"Expired\"."
                  },
                  "tone": {
                    "type": "string",
                    "enum": [
                      "warning",
                      "danger"
                    ]
                  }
                }
              }
            }
          },
          "is_sample_data": {
            "type": "boolean",
            "description": "A seeded demo waiver — the web marks the row \"Sample\"."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MessagingRules": {
        "type": "object",
        "description": "The organization's Messages rules as they apply to the caller — the same rules the server enforces on POST /messaging/threads, message attachments and the nightly retention purge.",
        "properties": {
          "can_start": {
            "type": "boolean",
            "description": "May the caller start a new conversation? When false, POST /messaging/threads answers 403 `thread_not_created`. Replying to a conversation the caller is already in is never limited by it."
          },
          "start_policy": {
            "type": "string",
            "description": "Who may start a new conversation, as one sentence to show the caller (e.g. \"Managers and above can start a new conversation; everyone can reply.\")."
          },
          "group_messages": {
            "type": "boolean",
            "description": "Are group conversations (more than one recipient) turned on?"
          },
          "attachments": {
            "type": "boolean",
            "description": "May files be attached to messages?"
          },
          "retention_days": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3650,
            "description": "Messages older than this many days are removed nightly; 0 means messages are kept forever."
          }
        }
      },
      "MessagingReactionOption": {
        "type": "object",
        "properties": {
          "emoji": {
            "type": "string",
            "description": "The emoji glyph a client submits to POST .../reactions."
          },
          "key": {
            "type": "string",
            "nullable": true,
            "example": "like",
            "description": "The reaction's catalog key; null for a glyph outside the catalog."
          },
          "label": {
            "type": "string",
            "description": "Human-readable name for the picker / aria-label (e.g. \"Like\")."
          },
          "icon_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL of the drawn icon the web shows for this reaction. null ⇒ draw the emoji glyph."
          }
        }
      },
      "MessagingReactions": {
        "type": "object",
        "description": "Emoji reaction state for a message (mirrors the web thread pills).",
        "properties": {
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Per-emoji tally, e.g. { \"👍\": 4, \"❤️\": 2 }. Empty when there are no reactions."
          },
          "mine": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The emoji the CALLER has left on this message (render these pills active)."
          },
          "total": {
            "type": "integer",
            "description": "Total reaction count across all emoji."
          }
        }
      },
      "MomentShare": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "subject_type",
          "shared",
          "page_open"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 91
          },
          "kind": {
            "type": "string",
            "enum": [
              "recognition",
              "award",
              "anniversary",
              "contest",
              "certificate",
              "year"
            ],
            "example": "award"
          },
          "kind_label": {
            "type": "string",
            "description": "What the kind is called, in the caller's language.",
            "example": "Award"
          },
          "subject_type": {
            "type": "string",
            "description": "The `subject_type` a POST names this moment by.",
            "enum": [
              "recognition",
              "award",
              "anniversary",
              "contest_win",
              "certificate",
              "year_in_recognition"
            ],
            "example": "award"
          },
          "subject_id": {
            "type": "integer",
            "nullable": true,
            "description": "The record's id; null for a year in recognition.",
            "example": 4812
          },
          "year": {
            "type": "integer",
            "nullable": true,
            "description": "The year, for a year in recognition.",
            "example": null
          },
          "shared": {
            "type": "boolean",
            "description": "The person shares it (not stopped).",
            "example": true
          },
          "page_open": {
            "type": "boolean",
            "description": "The page and its card answer now — shared, the moment still live and theirs, and sharing on.",
            "example": true
          },
          "public_url": {
            "type": "string",
            "nullable": true,
            "description": "The link to post — the moment's public page, or for a certificate its verification page. Null while the page is not open.",
            "example": "https://acme.workforce.mangoapps.com/moments/Zx9q3V1rT0aBcD2eFgH4iJ5kL6mN7oP8"
          },
          "card_image_url": {
            "type": "string",
            "nullable": true,
            "description": "The card picture (PNG). Null while the page is not open.",
            "example": "https://acme.workforce.mangoapps.com/moments/card/eyJfcmFpbHMiOnsibWVzc2FnZSI6.png"
          },
          "linkedin_share_url": {
            "type": "string",
            "nullable": true,
            "description": "LinkedIn's share link for `public_url`. Null while the page is not open.",
            "example": "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Facme.workforce.mangoapps.com%2Fmoments%2FZx9q"
          },
          "views_count": {
            "type": "integer",
            "description": "How many times the public page was opened.",
            "example": 12
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "shared_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stopped_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MuscleMemoryPaginationMeta": {
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total",
          "total_pages"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "description": "The page returned (a request past the end is clamped to the last page)."
          },
          "per_page": {
            "type": "integer",
            "description": "Rows per page used."
          },
          "total": {
            "type": "integer",
            "description": "Lessons across all pages for the filters given."
          },
          "total_pages": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "MuscleMemoryLesson": {
        "type": "object",
        "description": "One lesson. Lists and reads only ever show `active` and `superseded` lessons; `forgotten` appears only in the response to the call that forgets one.",
        "required": [
          "id",
          "claim",
          "domain",
          "category",
          "status",
          "confidence",
          "verified",
          "evidence_count",
          "reinforcement_count",
          "source",
          "recall_count",
          "used_count",
          "recall_audit_enabled",
          "due_for_recheck"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "claim": {
            "type": "string",
            "description": "The lesson, as one sentence."
          },
          "domain": {
            "type": "string",
            "description": "Operational domain, for example `safety_hub` or `service_desk`."
          },
          "category": {
            "type": "string",
            "description": "What kind of lesson it is (for example `manual_lesson` for one a person added)."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "superseded",
              "forgotten"
            ],
            "description": "`active` lessons can inform Ask AI answers (when confident enough); `superseded` ones were superseded by later evidence but stay listed and can be reinstated; `forgotten` ones are gone for good."
          },
          "confidence": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "maximum": 1,
            "description": "How strongly the business trusts the lesson, two decimals."
          },
          "verified": {
            "type": "boolean",
            "description": "True once a person has confirmed, corrected or reinstated it."
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "verified_by": {
            "type": "string",
            "nullable": true,
            "description": "Name of the person who last confirmed, corrected or reinstated it."
          },
          "correction_note": {
            "type": "string",
            "nullable": true,
            "description": "The curator's note from the last correction, if any."
          },
          "evidence_count": {
            "type": "integer",
            "description": "How many pieces of evidence back the lesson."
          },
          "reinforcement_count": {
            "type": "integer",
            "description": "How many times later evidence or a repeated sentence reinforced it."
          },
          "source": {
            "type": "string",
            "description": "What learned it — `manual` for a lesson a person added, otherwise the name of the distiller."
          },
          "learned_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_referenced_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Last time recall offered the lesson to an agent."
          },
          "recall_count": {
            "type": "integer",
            "description": "How many times recall offered the lesson to an agent. 0 for every lesson when `recall_audit_enabled` is false."
          },
          "used_count": {
            "type": "integer",
            "description": "How many times an agent declared it relied on the lesson. 0 when `recall_audit_enabled` is false."
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Last time an agent declared it relied on the lesson."
          },
          "recall_audit_enabled": {
            "type": "boolean",
            "description": "Whether the business keeps a usage history. When false, `recall_count`, `used_count` and `influence_history` read as empty for every lesson — it does not mean this lesson was never used."
          },
          "due_for_recheck": {
            "type": "boolean",
            "description": "True for a hand-written active lesson confirmed more than six months ago and not offered to an answer since — the app's \"Due for a re-check\" list (`status=recheck`). Confirming it again renews it."
          }
        }
      },
      "MuscleMemoryInfluenceEvent": {
        "type": "object",
        "required": [
          "recalled_at",
          "surface"
        ],
        "properties": {
          "recalled_at": {
            "type": "string",
            "format": "date-time"
          },
          "surface": {
            "type": "string",
            "description": "Where it was used, for example `ask_ai`."
          },
          "agent": {
            "type": "string",
            "description": "The agent that used it. Absent when not recorded."
          },
          "domain": {
            "type": "string",
            "description": "Domain of the question. Absent when not recorded."
          },
          "user": {
            "type": "string",
            "description": "Who received the answer. Present only for a caller who is a manager or above and only when the business records it (off by default); otherwise the key is absent."
          }
        }
      },
      "MuscleMemoryLessonDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MuscleMemoryLesson"
          },
          {
            "type": "object",
            "required": [
              "influence_history"
            ],
            "properties": {
              "influence_history": {
                "type": "array",
                "description": "The most recent uses of this lesson, newest first, at most five.",
                "items": {
                  "$ref": "#/components/schemas/MuscleMemoryInfluenceEvent"
                }
              }
            }
          }
        ]
      },
      "MuscleMemoryLessonList": {
        "type": "object",
        "required": [
          "lessons"
        ],
        "properties": {
          "lessons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MuscleMemoryLesson"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/MuscleMemoryPaginationMeta"
          },
          "count": {
            "type": "integer",
            "description": "Search only. The number of lessons returned (not the number that match)."
          },
          "per_page": {
            "type": "integer",
            "description": "Search only. The cap that applied (default 10, at most 50)."
          },
          "truncated": {
            "type": "boolean",
            "description": "Search only. True when more lessons match than were returned. `page` does not reach them — narrow the query or add filters."
          }
        }
      },
      "MuscleMemoryCreateResult": {
        "type": "object",
        "required": [
          "outcome",
          "lesson"
        ],
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "created",
              "reinforced"
            ],
            "description": "`created` (HTTP 201) for a new lesson; `reinforced` (HTTP 200) when the sentence matched a lesson the business already holds and that lesson was reinforced instead."
          },
          "lesson": {
            "$ref": "#/components/schemas/MuscleMemoryLesson"
          }
        }
      },
      "MuscleMemoryCurationResult": {
        "type": "object",
        "required": [
          "lesson"
        ],
        "properties": {
          "lesson": {
            "$ref": "#/components/schemas/MuscleMemoryLesson"
          },
          "notice": {
            "type": "string",
            "description": "Present when the lesson is still `superseded` after the call — it says the review or note was recorded but the lesson stays out of answers until it is reinstated — when the call changed nothing (`already_verified`, `already_forgotten`), and on `DELETE` of a lesson Contracts proposed for automatic approval, where it says an approval already accepted in Contracts keeps working. Absent otherwise."
          },
          "already_verified": {
            "type": "boolean",
            "description": "Present (true) only on `verify` of a lesson already confirmed and not due for a re-check: nothing changed."
          },
          "already_forgotten": {
            "type": "boolean",
            "description": "Present (true) only on `DELETE` of a lesson already forgotten: nothing changed."
          }
        }
      },
      "MuscleMemoryStats": {
        "type": "object",
        "description": "Aggregate figures for the business's real (non-sample) lessons.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Active lessons."
          },
          "verified": {
            "type": "integer",
            "description": "Active lessons a person has confirmed."
          },
          "unverified": {
            "type": "integer",
            "description": "Active lessons nobody has confirmed yet."
          },
          "superseded": {
            "type": "integer",
            "description": "Superseded lessons."
          },
          "forgotten": {
            "type": "integer",
            "description": "Lessons that have been forgotten (a count only; they are never listed)."
          },
          "avg_confidence": {
            "type": "string",
            "nullable": true,
            "description": "Average confidence of the active lessons as a decimal string with two places (for example `\"0.74\"`); null when there are none."
          },
          "by_domain": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Active lessons per domain, keyed by domain name."
          },
          "by_distiller": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Lessons per source, keyed by distiller name (`manual` for lessons a person added). Counts every status except samples."
          },
          "minted_last_30": {
            "type": "integer",
            "description": "Lessons created in the last `minted_window_days` days."
          },
          "minted_window_days": {
            "type": "integer"
          },
          "most_recalled": {
            "type": "array",
            "description": "The most recently used active lessons (at most five), newest use first.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "claim"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "claim": {
                  "type": "string"
                },
                "last_used_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "last_referenced_at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                }
              }
            }
          },
          "recalled_total": {
            "type": "integer",
            "description": "Active lessons an agent has ever relied on."
          },
          "recalled_last_30": {
            "type": "integer",
            "description": "Active lessons an agent relied on in the last `recall_window_days` days."
          },
          "considered_last_30": {
            "type": "integer",
            "description": "Active lessons recall offered to an agent in the last `recall_window_days` days."
          },
          "recall_window_days": {
            "type": "integer"
          },
          "impact": {
            "type": "object",
            "description": "What the lessons that intervened measurably changed.",
            "properties": {
              "measured": {
                "type": "integer",
                "description": "Interventions with a measured before/after result."
              },
              "improved": {
                "type": "integer",
                "description": "How many of those moved in the good direction."
              },
              "pending": {
                "type": "integer",
                "description": "Interventions still waiting for their after-measurement."
              },
              "avg_delta": {
                "type": "number",
                "nullable": true,
                "description": "Average measured change, direction-adjusted; null when nothing is measured."
              }
            }
          },
          "playbook_reliability": {
            "type": "object",
            "description": "How reliably the playbooks these lessons graduate into complete.",
            "properties": {
              "window_days": {
                "type": "integer"
              },
              "runs": {
                "type": "integer",
                "description": "Playbook runs that finished (completed or failed) in the window."
              },
              "completed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "success_rate": {
                "type": "number",
                "nullable": true,
                "description": "completed / runs, three decimals; null when there were no runs."
              }
            }
          }
        }
      },
      "NewsFeedPostFeedback": {
        "type": "object",
        "description": "One piece of private feedback on a post, from the caller's side\n(NewsFeed::PostFeedback#api_payload). The `can_*` flags are what the\ncaller may do now — all false on a token without `write:news_feed`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "resolved"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "received",
              "sent"
            ],
            "description": "received = the caller is the post's author."
          },
          "body": {
            "type": "string"
          },
          "reply_body": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "replied_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "read": {
            "type": "boolean",
            "description": "The author has opened it."
          },
          "feed": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              }
            }
          },
          "sender": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "recipient": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "can_reply": {
            "type": "boolean"
          },
          "can_resolve": {
            "type": "boolean"
          },
          "can_reopen": {
            "type": "boolean"
          },
          "can_withdraw": {
            "type": "boolean"
          }
        }
      },
      "NewsFeedEmojiKey": {
        "type": "string",
        "description": "One of the 14 News Feed reaction keys. The first eight are the `expressive` tier, the last six `action_oriented`. A business's reaction palette offers a subset (by default the eight expressive keys).",
        "enum": [
          "like",
          "love",
          "celebrate",
          "wow",
          "sad",
          "angry",
          "support",
          "insightful",
          "acknowledged",
          "need_clarification",
          "concern",
          "disagree",
          "needs_discussion",
          "escalate"
        ]
      },
      "NewsFeedReactorUser": {
        "type": "object",
        "nullable": true,
        "description": "The person who reacted; null when their account is gone.",
        "required": [
          "id",
          "name",
          "avatar_url"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Full name, else the account name."
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "description": "Full URL of the person's avatar — their uploaded photo on the API's own host, else a ui-avatars.com tile drawing their initials."
          }
        }
      },
      "NewsFeedReactor": {
        "type": "object",
        "required": [
          "user",
          "emoji_key",
          "tier",
          "reacted_at"
        ],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/NewsFeedReactorUser"
          },
          "emoji_key": {
            "$ref": "#/components/schemas/NewsFeedEmojiKey"
          },
          "tier": {
            "type": "string",
            "enum": [
              "expressive",
              "action_oriented"
            ]
          },
          "reacted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the reaction was set or last swapped."
          }
        }
      },
      "NewsFeedReactorGroup": {
        "type": "object",
        "required": [
          "emoji_key",
          "tier",
          "count",
          "users"
        ],
        "properties": {
          "emoji_key": {
            "$ref": "#/components/schemas/NewsFeedEmojiKey"
          },
          "tier": {
            "type": "string",
            "enum": [
              "expressive",
              "action_oriented"
            ]
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "description": "Reactors in this bucket (within the 200 returned), including any whose account is gone."
          },
          "users": {
            "type": "array",
            "description": "The bucket's reactors, most recent first; reactors whose account is gone are left out.",
            "items": {
              "$ref": "#/components/schemas/NewsFeedReactorUser"
            }
          }
        }
      },
      "NewsFeedReactorList": {
        "type": "object",
        "required": [
          "reactors",
          "grouped",
          "total"
        ],
        "properties": {
          "reactors": {
            "type": "array",
            "description": "Most recent first, at most 200.",
            "items": {
              "$ref": "#/components/schemas/NewsFeedReactor"
            }
          },
          "grouped": {
            "type": "array",
            "description": "The same reactors by emoji, largest bucket first.",
            "items": {
              "$ref": "#/components/schemas/NewsFeedReactorGroup"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "maximum": 200,
            "description": "Number of entries in `reactors` (capped at 200 with it)."
          }
        }
      },
      "NewsFeedReactionRecord": {
        "type": "object",
        "description": "One person's reaction on a post or a comment; exactly one of `feed_id` / `comment_id` is set.",
        "required": [
          "id",
          "feed_id",
          "comment_id",
          "emoji_key",
          "tier",
          "user_id",
          "reacted_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer",
            "nullable": true,
            "description": "Set for a reaction on a post; null on a comment's."
          },
          "comment_id": {
            "type": "integer",
            "nullable": true,
            "description": "Set for a reaction on a comment; null on a post's."
          },
          "emoji_key": {
            "$ref": "#/components/schemas/NewsFeedEmojiKey"
          },
          "tier": {
            "type": "string",
            "enum": [
              "expressive",
              "action_oriented"
            ]
          },
          "user_id": {
            "type": "integer"
          },
          "reacted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the reaction was set or last swapped."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NewsFeedMentionableUser": {
        "type": "object",
        "required": [
          "id",
          "name",
          "avatar_url"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "With `q`, the person's display name (never blank). With a blank `q`, their full name, else the account name."
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Full URL of the person's avatar — their photo, else an initials tile."
          },
          "meta": {
            "type": "string",
            "description": "Only with `q`. The secondary line under the name: job title and work location (each only as far as the People directory shows it to the caller) and email, joined by \" · \". May be an empty string."
          },
          "detail": {
            "type": "string",
            "description": "Only with `q`, and only when the caller may see job titles in the directory and the person has one: their job title."
          }
        }
      },
      "NewsFeedPostAnalytics": {
        "type": "object",
        "required": [
          "feed_id",
          "headline",
          "published_at",
          "must_read",
          "metrics",
          "timeline"
        ],
        "properties": {
          "feed_id": {
            "type": "integer"
          },
          "headline": {
            "type": "string",
            "nullable": true,
            "description": "The post's headline as plain text (its body's first line)."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null while the post has not gone out."
          },
          "must_read": {
            "type": "boolean",
            "description": "Whether the post is a must-read — only a must-read collects acknowledgements, so a client shows the Acknowledged figure only when this is true."
          },
          "metrics": {
            "$ref": "#/components/schemas/NewsFeedPostAnalyticsMetrics"
          },
          "timeline": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NewsFeedAnalyticsDay"
            }
          },
          "names_hidden": {
            "$ref": "#/components/schemas/NewsFeedAnalyticsNamesHidden"
          }
        }
      },
      "NewsFeedPostAnalyticsMetrics": {
        "type": "object",
        "required": [
          "audience_size",
          "read_count",
          "read_pct",
          "read_pct_label",
          "acknowledged_count",
          "acknowledged_pct",
          "acknowledged_pct_label",
          "reactions_count",
          "action_item_total",
          "action_item_completed_count",
          "action_completion_pct",
          "last_recomputed_at"
        ],
        "properties": {
          "audience_size": {
            "type": "integer",
            "description": "People in the post's audience snapshot (the named people only, while `names_hidden` is present)."
          },
          "read_count": {
            "type": "integer",
            "description": "Audience members with a qualified read (scrolled 80% or more)."
          },
          "read_pct": {
            "type": "number",
            "format": "float",
            "description": "read_count / audience_size × 100, one decimal; 0.0 with no audience."
          },
          "read_pct_label": {
            "type": "string",
            "nullable": true,
            "description": "The rate as a person reads it — a whole number with `%`, `<1%` / `>99%` at the ends; null with no audience.",
            "example": "33%"
          },
          "acknowledged_count": {
            "type": "integer",
            "description": "Audience members who acknowledged the post."
          },
          "acknowledged_pct": {
            "type": "number",
            "format": "float"
          },
          "acknowledged_pct_label": {
            "type": "string",
            "nullable": true,
            "example": ">99%"
          },
          "reactions_count": {
            "type": "integer",
            "description": "Reactions on the post itself (not its comments)."
          },
          "action_item_total": {
            "type": "integer",
            "description": "Action items raised on the post or its comments."
          },
          "action_item_completed_count": {
            "type": "integer",
            "description": "Action items someone has marked done."
          },
          "action_completion_pct": {
            "type": "number",
            "format": "float",
            "description": "0.0 when there are no action items."
          },
          "last_recomputed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NewsFeedAnalyticsDay": {
        "type": "object",
        "required": [
          "date",
          "read_pct",
          "ack_pct"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "description": "A day in the business's time zone."
          },
          "read_pct": {
            "type": "number",
            "format": "float",
            "description": "Cumulative read rate at the end of the day, one decimal."
          },
          "ack_pct": {
            "type": "number",
            "format": "float",
            "description": "Cumulative acknowledgement rate at the end of the day, one decimal."
          }
        }
      },
      "NewsFeedAnalyticsNamesHidden": {
        "type": "object",
        "description": "Present only while a works council hides some of the audience's names and that group is under the company's smallest group. The figures beside it count only the people who may be named.",
        "required": [
          "count",
          "group_count",
          "reason"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "nullable": true,
            "description": "Always null here — the hidden people are not counted one by one."
          },
          "group_count": {
            "type": "integer",
            "nullable": true,
            "description": "The hidden group's size; null while it is under the company's smallest group."
          },
          "fewer_than": {
            "type": "integer",
            "description": "Present instead of a size while the group is under the company's smallest group — that floor."
          },
          "reason": {
            "type": "string",
            "example": "Names are hidden for people in Germany (works council)."
          }
        }
      },
      "NewsFeedKnowledgeBaseEntry": {
        "type": "object",
        "required": [
          "id",
          "feed_id",
          "comment_id",
          "question_title",
          "answer_excerpt",
          "answerer_name",
          "segment",
          "marked_at",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "feed_id": {
            "type": "integer",
            "description": "The question post."
          },
          "comment_id": {
            "type": "integer",
            "description": "The comment marked as the answer."
          },
          "question_title": {
            "type": "string",
            "description": "The question post's headline, at most 500 characters (empty when the post is gone)."
          },
          "answer_excerpt": {
            "type": "string",
            "description": "The answer text, at most 500 characters."
          },
          "answerer_name": {
            "type": "string",
            "nullable": true,
            "description": "The person who gave the answer; null when there is none."
          },
          "segment": {
            "type": "string",
            "nullable": true,
            "description": "The question post's audience — \"All employees\", or the name of the first group it was aimed at; null when neither applies."
          },
          "marked_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the answer was marked correct."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "outdated"
            ]
          }
        }
      },
      "NewsFeedKnowledgeBaseAdminEntry": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NewsFeedKnowledgeBaseEntry"
          },
          {
            "type": "object",
            "required": [
              "outdated_at",
              "outdated_by_user_id",
              "outdated_reason"
            ],
            "properties": {
              "outdated_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "outdated_by_user_id": {
                "type": "integer",
                "nullable": true
              },
              "outdated_reason": {
                "type": "string",
                "nullable": true
              }
            }
          }
        ]
      },
      "NewsFeedKnowledgeBaseExportRow": {
        "type": "object",
        "required": [
          "id",
          "question_title",
          "question_body",
          "answer_text",
          "answerer_name",
          "segment",
          "marked_at",
          "status",
          "outdated_at",
          "outdated_reason"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "question_title": {
            "type": "string",
            "description": "At most 500 characters."
          },
          "question_body": {
            "type": "string",
            "description": "At most 2,000 characters."
          },
          "answer_text": {
            "type": "string",
            "description": "At most 2,000 characters."
          },
          "answerer_name": {
            "type": "string",
            "nullable": true,
            "description": "The answerer's full name, else their email."
          },
          "segment": {
            "type": "string",
            "nullable": true
          },
          "marked_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "outdated"
            ]
          },
          "outdated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "outdated_reason": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "NewsFeedSmeExpert": {
        "type": "object",
        "required": [
          "id",
          "name",
          "title",
          "topic_id",
          "designation"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The expert's user id."
          },
          "name": {
            "type": "string",
            "description": "Full name, else name, else \"Unnamed user\"."
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Job title."
          },
          "topic_id": {
            "type": "integer"
          },
          "designation": {
            "type": "string",
            "enum": [
              "assigned",
              "measured"
            ],
            "description": "`assigned` — an admin named them on the SMEs page; `measured` — the weekly scoring ranks them an expert."
          }
        }
      },
      "NewsFeedKnowledgeGap": {
        "type": "object",
        "required": [
          "topic_id",
          "topic_name",
          "sme_count",
          "post_volume_30d",
          "severity",
          "detected_at",
          "needs_experts"
        ],
        "properties": {
          "topic_id": {
            "type": "integer"
          },
          "topic_name": {
            "type": "string",
            "description": "The topic's name. Only active topics are listed."
          },
          "sme_count": {
            "type": "integer",
            "description": "Experts the topic has now — added by an admin or found automatically, each person once."
          },
          "post_volume_30d": {
            "type": "integer",
            "description": "Published posts on the topic in the last 30 days."
          },
          "severity": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "detected_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this answer was computed (the time of the request)."
          },
          "needs_experts": {
            "type": "boolean",
            "description": "True for `high` and `medium` — the topics the Experts page lists under \"Topics that need experts\"."
          }
        }
      },
      "NewsFeedCcPreference": {
        "type": "object",
        "required": [
          "cc_preference",
          "effective"
        ],
        "properties": {
          "cc_preference": {
            "type": "string",
            "enum": [
              "auto",
              "on",
              "off"
            ],
            "description": "What the caller chose (`auto` until they choose)."
          },
          "effective": {
            "type": "string",
            "enum": [
              "on",
              "off"
            ],
            "description": "What the player should do now — `auto` resolved against the caller's language."
          }
        }
      },
      "NewsFeedReadAloudPreference": {
        "type": "object",
        "required": [
          "tts_voice",
          "read_aloud_speed"
        ],
        "properties": {
          "tts_voice": {
            "type": "string",
            "nullable": true,
            "enum": [
              "neutral",
              "male",
              "female",
              null
            ],
            "description": "The caller's voice; null = the default."
          },
          "read_aloud_speed": {
            "type": "number",
            "nullable": true,
            "enum": [
              0.75,
              1.0,
              1.25,
              1.5,
              2.0,
              null
            ],
            "description": "The caller's speed; null = normal speed (1.0).",
            "example": 1.25
          }
        }
      },
      "NewsFeedVoiceTranscript": {
        "type": "object",
        "required": [
          "id",
          "status",
          "transcript_text",
          "failure_reason"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "transcript_text": {
            "type": "string",
            "nullable": true,
            "description": "The text, once `completed`."
          },
          "failure_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why it `failed`, as a sentence to show the person — \"No speech detected in the recording.\", the AI usage policy's sentence, or \"We couldn't transcribe this recording. Please try again.\" after three failed attempts."
          }
        }
      },
      "NewsFeedTtsAudio": {
        "type": "object",
        "required": [
          "audio_url",
          "expires_at",
          "cached"
        ],
        "properties": {
          "audio_url": {
            "type": "string",
            "format": "uri",
            "description": "An MP3 to stream — a signed link valid for one hour."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When `audio_url` stops working — one hour after this call, or sooner when the cached audio expires first (24 hours after it was generated)."
          },
          "cached": {
            "type": "boolean",
            "description": "True when the audio was already generated (nobody was charged)."
          },
          "language": {
            "type": "string",
            "description": "The language the audio speaks — `en` for the post's original text, else the translation's language code.",
            "example": "ko"
          },
          "title": {
            "type": "string",
            "description": "The post's headline in that language, for the player."
          }
        }
      },
      "NewsFeedAiRateLimited": {
        "type": "object",
        "description": "The per-person hourly limit was reached. Answered with HTTP 200.",
        "required": [
          "ok",
          "error",
          "code"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string",
            "example": "You've reached the hourly limit for this feature. Try again after 3:00 PM."
          },
          "code": {
            "type": "string",
            "enum": [
              "rate_limited"
            ]
          },
          "retry_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the hourly limit lifts (the start of the next UTC hour), the same moment the `error` sentence names."
          }
        }
      },
      "NewsFeedSubtitleStatus": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "failed",
              "not_applicable",
              "not_found"
            ]
          }
        }
      },
      "NotesCreateRequest": {
        "type": "object",
        "description": "Top-level fields (not nested under a `note` key).",
        "required": [
          "content",
          "notable_type",
          "notable_id"
        ],
        "properties": {
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5000
          },
          "notable_type": {
            "type": "string",
            "enum": [
              "PerformanceReview",
              "Goal",
              "Apps::Epms::Meeting",
              "DevelopmentPlan",
              "User"
            ],
            "description": "The kind of record the note hangs on. Alias `entity_type`."
          },
          "notable_id": {
            "type": "integer",
            "description": "The record's id in this business. Alias `entity_id`."
          },
          "entity_type": {
            "type": "string",
            "description": "Alias of `notable_type`, used when `notable_type` is absent."
          },
          "entity_id": {
            "type": "integer",
            "description": "Alias of `notable_id`, used when `notable_id` is absent."
          },
          "note_type": {
            "type": "string",
            "enum": [
              "general",
              "observation",
              "goal_progress",
              "development",
              "feedback",
              "preparation",
              "meeting_notes",
              "action_item",
              "reminder"
            ],
            "default": "general"
          },
          "visibility": {
            "type": "string",
            "description": "Who may read the note: `private` (the author), `shared` (the author and the person it is about), `manager_only` (managers and above) or `public` (all participants). The enum names `note_private` and `note_public` are accepted too. Defaults to private.",
            "enum": [
              "private",
              "note_private",
              "shared",
              "manager_only",
              "public",
              "note_public"
            ],
            "default": "note_private"
          }
        }
      },
      "NotesRefusal": {
        "type": "object",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "message": {
            "type": "string"
          }
        }
      },
      "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 — in the user's language when the business translates the web app.",
            "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 — in the user's language when the business translates the web app. Match on `key`, never on the label.\n",
            "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": "Since when the longest-waiting ask in this entry has waited (UTC ISO 8601), the time the \"longest waiting first\" order reads: when the ask arrived, or, for a reminder that rolls several items up and states its oldest's age (\"15 form submissions … the oldest for 12 days\"), when that oldest item began waiting.\n",
            "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": [
              "unread_first_then_newest"
            ],
            "description": "The order `sections` is already in — subsections with something unread first, each set newest 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",
            "description": "In the user's language when the business translates the web app — match on `key`.",
            "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"
          },
          "link_withheld_app": {
            "type": "string",
            "nullable": true,
            "description": "When `action_url` is null because the app the notification leads into will not let this reader in, that app's name — so a client can say \"This opens in <App>, which isn't open to you\" rather than draw a row that silently does nothing. Null otherwise.\n",
            "example": "Asset Pro"
          },
          "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": {}
          },
          "actions": {
            "type": "array",
            "description": "The row's open inline actions (approve, acknowledge, a link, …), in\norder. A `link` action OPENS `url`; every other kind is performed\nwith `POST /notifications/{notification_id}/actions/{id}/perform`.\nA decision (`approve`, `reject`, `resume`, `sign`) needs a token with\nthe `write:approvals` scope, and every other kind (an acknowledgment,\nan attestation) `write:notifications` — the native app's sign-in\ntokens carry both; without it `perform` answers 403\n`insufficient_permissions`.\n\nDelete-level (owner decisions 2026-10-04, #6659 and #6721): EVERY\ndecline — a decision that ends someone else's request — also requires\nthe delete scope of the area the request belongs to (declining a leave\nrequest `destructive:leave_management`, a timesheet\n`destructive:timekeeping`, a contract `destructive:contracts`, an HR\ndocument `destructive:hr_file_manager`, an alert held for approval\n`destructive:safety_hub`, a broadcast `destructive:broadcasts`, …; an\nAI suggestion or an agent's paused step is the exception), and so do\napproving a nomination (`destructive:recognition`) and a store\ncancellation request's \"Cancel & refund\" (`destructive:company_store`);\nwithout it `perform` answers 403 `insufficient_permissions` naming the\nscope.\n",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "kind": {
                  "type": "string",
                  "example": "acknowledge"
                },
                "label": {
                  "type": "string",
                  "example": "I have read and accept"
                },
                "url": {
                  "type": "string",
                  "nullable": true,
                  "description": "`link` actions only (mobile path); null for every other kind."
                },
                "confirmations": {
                  "type": "array",
                  "description": "The statements the caller must affirm before `perform` runs\n(a Policy Hub or Job Descriptions acknowledgment is a legal\nattestation); `[]` when the action asks none. Show them and send\nthe affirmed keys back as `confirmations: [keys]` in the\n`perform` body — a missing required key is 422\n`confirmation_required` (`error.details.confirmations` lists them),\nand an impersonation token is refused 403\n`attestation_not_delegable` (only the person may attest).\n",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      },
                      "required": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          },
          "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"
          }
        }
      },
      "OffboardingUserRef": {
        "type": "object",
        "description": "A person on an offboarding plan (Api::V1::Offboarding::PlansController#serialize_user).",
        "required": [
          "id",
          "name",
          "email"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "The person's display name."
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "Null for a person with no email address on file."
          }
        }
      },
      "OffboardingPlanSummary": {
        "type": "object",
        "description": "One offboarding plan as `GET /offboarding/plans` lists it\n(`serialize_plan` without `detailed`).\n",
        "required": [
          "id",
          "plan_name",
          "plan_type",
          "status",
          "completion_percentage",
          "duration_days",
          "termination_date",
          "last_working_day",
          "notice_date",
          "voluntary",
          "user",
          "manager",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "plan_name": {
            "type": "string"
          },
          "plan_type": {
            "type": "string",
            "enum": [
              "voluntary",
              "involuntary",
              "retirement",
              "end_of_contract",
              "layoff"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "cancelled"
            ]
          },
          "completion_percentage": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Share of the plan's tasks finished, 0–100, as a JSON number.",
            "example": 20.8
          },
          "duration_days": {
            "type": "integer",
            "description": "The plan's planned length in days."
          },
          "termination_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "last_working_day": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "notice_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "voluntary": {
            "type": "boolean",
            "nullable": true
          },
          "user": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffboardingUserRef"
              }
            ],
            "description": "The departing employee."
          },
          "manager": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OffboardingUserRef"
              }
            ],
            "nullable": true,
            "description": "The plan's manager; null on older plans saved without one."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OffboardingTaskSummary": {
        "type": "object",
        "description": "One offboarding checklist task as `GET /offboarding/tasks` lists it\n(`serialize_task` without `detailed`).\n",
        "required": [
          "id",
          "plan_id",
          "task_name",
          "task_type",
          "department",
          "status",
          "priority",
          "required",
          "sequence_order",
          "due_date",
          "is_overdue",
          "days_remaining",
          "assigned_to",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "plan_id": {
            "type": "integer"
          },
          "task_name": {
            "type": "string"
          },
          "task_type": {
            "type": "string",
            "enum": [
              "access_revocation",
              "equipment_return",
              "documentation",
              "knowledge_transfer",
              "exit_interview",
              "compliance",
              "other",
              "final_paperwork",
              "benefits_termination"
            ]
          },
          "department": {
            "type": "string",
            "enum": [
              "IT",
              "HR",
              "Facilities",
              "Payroll",
              "Manager",
              "Legal",
              "Finance",
              "Operations"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "skipped",
              "blocked"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "critical",
              "high",
              "medium",
              "low"
            ],
            "nullable": true
          },
          "required": {
            "type": "boolean",
            "description": "Whether the task must be finished for the plan to complete."
          },
          "sequence_order": {
            "type": "integer",
            "nullable": true,
            "description": "Position in the plan's checklist. Not unique — two templates applied to one plan can repeat a number."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "is_overdue": {
            "type": "boolean",
            "description": "Past its due date, neither completed nor skipped, and its plan still under way (draft or active) — false on a completed or cancelled plan."
          },
          "days_remaining": {
            "type": "integer",
            "nullable": true,
            "description": "Days until the due date (negative when past it); null with no due date, once completed, or once its plan is completed or cancelled."
          },
          "assigned_to": {
            "type": "object",
            "nullable": true,
            "description": "The person the task is assigned to; null when unassigned.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OffboardingDocumentSummary": {
        "type": "object",
        "description": "One offboarding document as `GET /offboarding/documents` lists it\n(`serialize_document` without `detailed`).\n",
        "required": [
          "id",
          "plan_id",
          "document_name",
          "document_type",
          "status",
          "required",
          "sequence_order",
          "due_date",
          "is_overdue",
          "days_remaining",
          "requires_esignature",
          "esignature_status",
          "signature_in_progress",
          "signature_complete",
          "file_attached",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "plan_id": {
            "type": "integer"
          },
          "document_name": {
            "type": "string"
          },
          "document_type": {
            "type": "string",
            "enum": [
              "separation_agreement",
              "nda_reminder",
              "return_checklist",
              "exit_interview",
              "cobra_notice",
              "final_pay_stub",
              "benefits_summary",
              "exit_interview_form",
              "nda_acknowledgment",
              "benefits_information",
              "final_paycheck",
              "equipment_return",
              "reference_letter",
              "custom",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "uploaded",
              "verified",
              "rejected"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "sequence_order": {
            "type": "integer",
            "nullable": true,
            "description": "Position in the plan's document list. Not unique."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "is_overdue": {
            "type": "boolean",
            "description": "Past its due date, still pending, and its plan still under way (draft or active) — false on a completed or cancelled plan."
          },
          "days_remaining": {
            "type": "integer",
            "nullable": true,
            "description": "Days until the due date (negative when past it); null with no due date, once no longer pending, or once its plan is completed or cancelled."
          },
          "requires_esignature": {
            "type": "boolean",
            "nullable": true
          },
          "esignature_status": {
            "type": "string",
            "nullable": true,
            "description": "The e-signature state as stored, not validated against a fixed list.\nValues the app writes include `not_required`, `pending_signature`,\n`sent`, `viewed`, `signed`, `declined` and `expired`; null when no\nsignature workflow was ever set up.\n"
          },
          "signature_in_progress": {
            "type": "boolean",
            "description": "Sent for signature and not yet signed (`esignature_status` is `sent` or `viewed`)."
          },
          "signature_complete": {
            "type": "boolean",
            "description": "`esignature_status` is `signed`."
          },
          "file_attached": {
            "type": "boolean",
            "description": "A file has been uploaded to the document."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OfficeIntegrationApp": {
        "type": "object",
        "required": [
          "slug",
          "app_slug",
          "name",
          "icon",
          "supported_hosts",
          "capabilities"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "The record type's registry key — the `{app_slug}` the records paths take.",
            "example": "service-desk-help"
          },
          "app_slug": {
            "type": "string",
            "description": "The marketplace app the record type belongs to; several types may share it.",
            "example": "service-desk"
          },
          "name": {
            "type": "string",
            "description": "The app's name as the marketplace lists it, followed by the record type for an app with several (\"Service Desk — Help requests\").",
            "example": "Service Desk — Help requests"
          },
          "capabilities": {
            "type": "object",
            "required": [
              "save_revision"
            ],
            "properties": {
              "save_revision": {
                "type": "boolean",
                "description": "Whether `POST …/revisions` can save an edited document back for this type."
              }
            }
          },
          "icon": {
            "type": "string",
            "description": "A Font Awesome class list.",
            "example": "fas fa-file-contract"
          },
          "supported_hosts": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "word",
                "excel"
              ]
            }
          }
        }
      },
      "OfficeIntegrationStatus": {
        "type": "object",
        "nullable": true,
        "description": "The record's status badge; null for a record type without one.",
        "required": [
          "label",
          "variant"
        ],
        "properties": {
          "label": {
            "type": "string",
            "nullable": true,
            "example": "Pending approval"
          },
          "variant": {
            "type": "string",
            "description": "A Bootstrap colour variant (`success`, `warning`, `secondary`, …).",
            "example": "warning"
          }
        }
      },
      "OfficeIntegrationSearchResult": {
        "type": "object",
        "required": [
          "id",
          "title",
          "subtitle",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/OfficeIntegrationStatus"
          }
        }
      },
      "OfficeIntegrationField": {
        "type": "object",
        "required": [
          "label",
          "value"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "value": {
            "description": "The field's value as the record type renders it (usually a string; may be null)."
          },
          "variant": {
            "type": "string",
            "description": "Optional colour variant for the value."
          }
        }
      },
      "OfficeIntegrationAction": {
        "type": "object",
        "required": [
          "name",
          "label"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The `{name}` to post to `…/actions/{name}`.",
            "example": "approve"
          },
          "label": {
            "type": "string",
            "example": "Approve"
          },
          "variant": {
            "type": "string",
            "description": "Button colour variant (`primary`, `danger`, `secondary`, …)."
          },
          "requires_reason": {
            "type": "boolean",
            "description": "When true, collect a non-blank `reason` and send it in the body."
          },
          "reason_label": {
            "type": "string",
            "description": "The prompt shown above the reason box."
          },
          "reason_max": {
            "type": "integer",
            "description": "The longest reason the action accepts."
          }
        }
      },
      "OfficeIntegrationBlockedAction": {
        "type": "object",
        "required": [
          "name",
          "label",
          "reason"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Why the action cannot be taken here yet."
          }
        }
      },
      "OfficeIntegrationInsight": {
        "type": "object",
        "required": [
          "severity",
          "message"
        ],
        "properties": {
          "severity": {
            "type": "string",
            "example": "warning"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "OfficeIntegrationRecord": {
        "type": "object",
        "required": [
          "id",
          "title",
          "subtitle",
          "status",
          "fields",
          "actions",
          "blocked_actions",
          "ai_insights",
          "web_url",
          "etag",
          "capabilities"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/OfficeIntegrationStatus"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OfficeIntegrationField"
            }
          },
          "actions": {
            "type": "array",
            "description": "The actions this person and token may take now.",
            "items": {
              "$ref": "#/components/schemas/OfficeIntegrationAction"
            }
          },
          "blocked_actions": {
            "type": "array",
            "description": "Actions shown disabled with the reason — the record cannot take them yet, or the token's scopes do not cover them. Posting one is refused.",
            "items": {
              "$ref": "#/components/schemas/OfficeIntegrationBlockedAction"
            }
          },
          "ai_insights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OfficeIntegrationInsight"
            }
          },
          "web_url": {
            "type": "string",
            "nullable": true,
            "description": "The record's page in the web app, as a host-relative path."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "The record's current version tag — send it as `If-Match` when saving a revision."
          },
          "capabilities": {
            "type": "object",
            "required": [
              "save_revision"
            ],
            "properties": {
              "save_revision": {
                "type": "boolean",
                "description": "Whether `POST …/revisions` can save an edited document back for this record type."
              }
            }
          }
        }
      },
      "OfficeIntegrationRevisionResult": {
        "type": "object",
        "required": [
          "success"
        ],
        "additionalProperties": true,
        "description": "The record type's result; keys beyond `success` vary by type.",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "False when the record type refused the save (see `error`)."
          },
          "revision_id": {
            "type": "integer",
            "nullable": true
          },
          "version": {
            "type": "integer",
            "nullable": true,
            "description": "The new revision or version number."
          },
          "message": {
            "type": "string",
            "example": "Revision 3 saved"
          },
          "error": {
            "type": "string",
            "description": "Present when `success` is false — why the save was refused."
          },
          "ai_analysis": {
            "description": "Contracts only — the AI analysis of the new revision when one ran, else null."
          },
          "row_errors": {
            "type": "array",
            "description": "Spreadsheet record types — the rows that could not be read.",
            "items": {}
          },
          "previous_content_preview": {
            "type": "string",
            "description": "Offer letters only — the start of the letter's previous text."
          }
        }
      },
      "OfficeIntegrationActionResult": {
        "type": "object",
        "required": [
          "success"
        ],
        "additionalProperties": true,
        "description": "The action's result; keys beyond `success` vary by action.",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "message": {
            "type": "string"
          },
          "redirect_url": {
            "type": "string",
            "description": "`open_web` — the record's web page to open."
          }
        }
      },
      "PasswordManagerRefusal": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          },
          "error_code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PasswordManagerEntrySummary": {
        "type": "object",
        "description": "An entry's summary — never its password or TOTP secret.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "entry_type": {
            "type": "string"
          },
          "username": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "category_icon": {
            "type": "string",
            "nullable": true
          },
          "visibility": {
            "type": "string"
          },
          "favorite": {
            "type": "boolean"
          },
          "has_totp": {
            "type": "boolean"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_accessed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "owner": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "view_url": {
            "type": "string"
          }
        }
      },
      "PeopleChange": {
        "type": "object",
        "description": "One approved profile change as a write-back receiver sees it. Values are rebuilt at fetch time; none is stored.",
        "properties": {
          "change_id": {
            "type": "integer"
          },
          "record_type": {
            "type": "string",
            "description": "Absent for a profile change; `compliance_record` for a signed record (a policy acknowledgment), whose `change_type` is `policy_attestation`."
          },
          "change_type": {
            "type": "string",
            "description": "The profile field the change writes (phone, full_address, department_id, job_title, custom_… ), or `policy_attestation` for a signed record."
          },
          "label": {
            "type": "string",
            "description": "The field's label as admins see it."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "copy"
            ],
            "description": "owner — this system is the master for the field; copy — it only receives the change."
          },
          "subject": {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "integer"
              },
              "employee_id": {
                "type": "string",
                "nullable": true
              },
              "email": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "values": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "Attribute → value (dates ISO 8601, blank as null). full_address carries address, city, state, zip and, when set, address_line2 and country."
          },
          "effective_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "sensitive": {
            "type": "boolean",
            "description": "The field is masked from third parties elsewhere in Workforce; handle the value accordingly."
          },
          "status": {
            "type": "string",
            "enum": [
              "dispatching",
              "unknown",
              "sent",
              "confirmed"
            ],
            "description": "The change's delivery state on the Workforce side."
          }
        }
      },
      "PolicyHubPolicySummary": {
        "type": "object",
        "description": "A policy as the list shows it. `id` opens it at `GET /policy_hub/policies/{id}`.",
        "required": [
          "id",
          "title",
          "policy_type",
          "requires_acknowledgment",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "policy_type": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "version_number": {
            "type": "integer",
            "nullable": true
          },
          "requires_acknowledgment": {
            "type": "boolean",
            "description": "Whether the policy asks its audience for an acknowledgment."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PolicyHubHandbookSummary": {
        "type": "object",
        "description": "A handbook as the list shows it. `id` opens it at `GET /policy_hub/handbooks/{id}`.",
        "required": [
          "id",
          "title",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "version_number": {
            "type": "integer",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PolicyHubPolicyDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "policy_type": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "version_number": {
            "type": "integer",
            "nullable": true
          },
          "requires_acknowledgment": {
            "type": "boolean"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "content": {
            "type": "string",
            "nullable": true
          },
          "asked": {
            "type": "boolean",
            "description": "Whether the policy is ASKING the caller to acknowledge it now: it asks for an acknowledgment and its current audience includes them. `deadline`, `overdue` and `days_until_deadline` describe an ask, so they are null / false when this is false."
          },
          "applicable_states": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Retired free-text states, kept for older clients. Read `applies_to`."
          },
          "applies_to": {
            "type": "object",
            "description": "Who the policy is for — its one Applies to rule (Knowledge Layer A2).",
            "properties": {
              "everyone": {
                "type": "boolean",
                "description": "True when it is for every employee."
              },
              "label": {
                "type": "string",
                "nullable": true,
                "description": "How the policy page names it.",
                "example": "Ohio, Indiana and Michigan · hourly"
              },
              "summary": {
                "type": "string",
                "nullable": true,
                "description": "The rule in words; null for everyone.",
                "example": "hourly, at stores and hub stores in Ohio, Indiana and Michigan"
              },
              "for_label": {
                "type": "string",
                "nullable": true,
                "description": "The chip a reader it is NOT for sees; null when it is for everyone.",
                "example": "For California employees"
              }
            }
          },
          "applies_to_you": {
            "type": "boolean",
            "description": "Whether the policy is for the caller. False also when a local version covers them — `your_local_version` then names it."
          },
          "language": {
            "type": "string",
            "description": "The language the policy is written in (its own \"Written in\", else English).",
            "example": "en"
          },
          "company_policy": {
            "type": "object",
            "nullable": true,
            "description": "LOCAL VERSIONS only (Knowledge Layer X2): the company policy this one is a local version of.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "version_number": {
                "type": "integer",
                "nullable": true
              }
            }
          },
          "company_version_number": {
            "type": "integer",
            "nullable": true,
            "description": "LOCAL VERSIONS only — the company policy version this local version reflects."
          },
          "company_version_changed": {
            "type": "boolean",
            "description": "LOCAL VERSIONS only — true when the company policy published a major or legal change past `company_version_number` (the local version has drifted and needs review)."
          },
          "your_local_version": {
            "type": "object",
            "nullable": true,
            "description": "COMPANY policies only — the caller's own local version when one covers them and they may read it (open and acknowledge that one); null otherwise.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "language": {
                "type": "string",
                "example": "es"
              }
            }
          },
          "key_facts": {
            "type": "array",
            "description": "The policy's approved key facts (Knowledge Layer A5) — approved in Verified Knowledge by an administrator or by whoever manages the policy — in the order they were read, only those in force: never one past its end date (`effective_until`), nor one read from a version the policy has materially changed since. The same facts Ask AI answers from. Empty when none are.",
            "items": {
              "$ref": "#/components/schemas/PolicyHubKeyFact"
            }
          },
          "user_acknowledged": {
            "type": "boolean",
            "description": "Whether the caller has accepted the CURRENT version (false for a row HR flagged for re-acknowledgment)."
          },
          "requires_re_acknowledgment": {
            "type": "boolean",
            "description": "True when HR flagged the caller's acceptance for re-acknowledgment (a new version). `acknowledged_at` then still holds the earlier acceptance while `user_acknowledged` is false."
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the caller last accepted the policy; null if never."
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the acknowledgment is due; null when none is set or the policy is not asking the caller (`asked` false)."
          },
          "overdue": {
            "type": "boolean",
            "description": "True when the policy is asking the caller and the deadline has passed."
          },
          "days_until_deadline": {
            "type": "integer",
            "nullable": true,
            "description": "Whole days until `deadline` (0 once it has passed); null when there is no deadline or `asked` is false."
          },
          "viewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the caller first viewed it (the reader's \"First viewed\" row; the key is `viewed_at` — there is no `first_viewed_at`). Recorded only for a policy that asks the caller for an acknowledgment, so null for a purely informational one."
          },
          "valid_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the caller's signature lapses and they will be asked to sign again (a policy with a renewal period); null when it does not lapse or they have not signed."
          },
          "receipt_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The caller's signature receipt (a PDF, opened in a browser signed in as them); null when they hold no signature on record."
          },
          "requires_signature": {
            "type": "boolean",
            "description": "True when the acknowledgment the caller OWES is a signature, taken at `sign_url` in a browser — `POST …/acknowledge` refuses it (`esignature_required`). False when they owe nothing (`asked` false, or already accepted and not asked again)."
          },
          "sign_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The signing step, when `requires_signature`; else null."
          },
          "comprehension_required": {
            "type": "boolean",
            "description": "True when the caller owes the acknowledgment and must first pass the policy's comprehension check, taken on the web at `check_url`."
          },
          "check_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The policy page holding the comprehension check, when `comprehension_required`; else null."
          },
          "required_training": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Titles of the training the caller must complete before they can acknowledge it; empty when none, or when they owe nothing."
          },
          "training_openable": {
            "type": "boolean",
            "description": "False when `required_training` is not empty and the caller cannot open the Training app (it is off for the company, or not open to them): show the titles as text, not links."
          },
          "training_access_notice": {
            "type": "string",
            "nullable": true,
            "description": "When `training_openable` is false, the sentence saying why and who can fix it, worded for the caller (an administrator is told they can); else null."
          }
        }
      },
      "PolicyHubKeyFact": {
        "type": "object",
        "description": "One key fact read from a policy and approved (Knowledge Layer A5).",
        "properties": {
          "key": {
            "type": "string",
            "example": "bereavement_leave_days_immediate_family"
          },
          "label": {
            "type": "string",
            "example": "Bereavement leave, immediate family"
          },
          "value": {
            "type": "string",
            "example": "3"
          },
          "unit": {
            "type": "string",
            "nullable": true,
            "example": "days"
          },
          "display_value": {
            "type": "string",
            "example": "3 days"
          },
          "effective_from": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "When the fact holds from, when the policy names a date."
          },
          "effective_until": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "When the fact stops holding, when the policy names one."
          },
          "as_of": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The date the fact holds from — its effective date, else the day its policy version was published."
          },
          "version_number": {
            "type": "integer",
            "nullable": true,
            "description": "The policy version the fact was read from."
          },
          "as_of_label": {
            "type": "string",
            "nullable": true,
            "example": "as of 1 Jan 2026 · v4"
          }
        }
      },
      "PolicyHubAcknowledgment": {
        "type": "object",
        "description": "One of the caller's acknowledgment rows (Api::V1::PolicyHub::BaseController#acknowledgment_json) — what `POST /policy_hub/policies/{id}/acknowledge` answers under `acknowledgment`, and each entry of `GET /policy_hub/acknowledgments`' `history`. `id` is the ACKNOWLEDGMENT's id; the policy is `policy_id`.",
        "required": [
          "id",
          "policy_id",
          "policy_title",
          "acknowledged",
          "acknowledged_at",
          "viewed_at",
          "requires_re_acknowledgment",
          "signed_earlier_version",
          "deadline"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The acknowledgment's id (not the policy's).",
            "example": 313372
          },
          "policy_id": {
            "type": "integer",
            "description": "The policy acknowledged — open it at `GET /policy_hub/policies/{policy_id}`.",
            "example": 60
          },
          "policy_title": {
            "type": "string",
            "nullable": true,
            "example": "Code of Conduct"
          },
          "acknowledged": {
            "type": "boolean",
            "description": "Whether the caller has accepted it. Stays true on a row HR flagged for re-acknowledgment — read `requires_re_acknowledgment` with it."
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the caller last accepted it; null while never accepted."
          },
          "viewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the caller first viewed the policy — the same field, under the same name, as the policy detail's `viewed_at` (there is no `first_viewed_at`). Accepting marks the row viewed, so an accepted row carries one."
          },
          "requires_re_acknowledgment": {
            "type": "boolean",
            "description": "True when the caller is being ASKED to accept the policy again (a new version, and the policy's current audience still includes them) — the policy is then also in `pending`. Accepting it again clears this."
          },
          "signed_earlier_version": {
            "type": "boolean",
            "description": "True when the row was flagged for a new version but the policy no longer asks the caller (they left its audience): the record of an earlier signature, not a call to action. Never true together with `requires_re_acknowledgment`."
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The row's acknowledgment deadline. Accepting clears it, so it is almost always null here — set only on a row flagged for re-acknowledgment. Read due/overdue state from `pending`, not from here."
          },
          "version_acknowledged": {
            "type": "integer",
            "nullable": true,
            "description": "The policy version the caller accepted."
          },
          "signature_method": {
            "type": "string",
            "nullable": true,
            "description": "How it was accepted — `click`, `esignature`, `electronic`, `onboarding_task` or `inbox_attestation`."
          },
          "valid_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the signature lapses and is asked for again; null when it does not."
          },
          "receipt_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "The caller's signature receipt (a PDF); null when none is on record."
          }
        }
      },
      "PolicyHubPendingAcknowledgment": {
        "description": "A policy still asking the caller for an acknowledgment (Api::V1::PolicyHub::BaseController#pending_acknowledgment_json): the policy summary — `id` is the POLICY id — plus the deadline state of the caller's acknowledgment row.",
        "allOf": [
          {
            "$ref": "#/components/schemas/PolicyHubPolicySummary"
          },
          {
            "type": "object",
            "required": [
              "deadline",
              "overdue",
              "days_until_deadline"
            ],
            "properties": {
              "deadline": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the acknowledgment is due; null when no deadline is set."
              },
              "overdue": {
                "type": "boolean",
                "description": "True once `deadline` has passed."
              },
              "days_until_deadline": {
                "type": "integer",
                "nullable": true,
                "description": "Whole days until `deadline` (0 once it has passed); null when there is no deadline."
              },
              "requires_re_acknowledgment": {
                "type": "boolean",
                "description": "True when the caller accepted it before and is asked to accept it again."
              },
              "re_acknowledgment_reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "policy_updated",
                  "signature_renewal",
                  null
                ],
                "description": "Why it is asked again: `policy_updated` (a new version changed it) or `signature_renewal` (the signature lapsed; the text may be unchanged). Null for a first acknowledgment."
              }
            }
          }
        ]
      },
      "PolicyHubHandbookDetail": {
        "description": "A handbook as `GET /policy_hub/handbooks/{id}` returns it: the summary plus its introduction and sections.",
        "allOf": [
          {
            "$ref": "#/components/schemas/PolicyHubHandbookSummary"
          },
          {
            "type": "object",
            "required": [
              "introduction",
              "sections"
            ],
            "properties": {
              "introduction": {
                "type": "string",
                "nullable": true
              },
              "my_acknowledgment": {
                "type": "object",
                "nullable": true,
                "description": "The caller's standing on the policies this handbook asks them to acknowledge, as the handbook page shows it; null when it asks them for none.",
                "properties": {
                  "total": {
                    "type": "integer",
                    "description": "Policies in it that ask the caller."
                  },
                  "acknowledged": {
                    "type": "integer",
                    "description": "Of those",
                    "how many the caller has accepted.": null
                  },
                  "outstanding": {
                    "type": "integer",
                    "description": "Of those",
                    "how many are still owed.": null
                  },
                  "portal_outstanding": {
                    "type": "integer",
                    "description": "Of the outstanding",
                    "how many can be signed from the handbook's public page.": null
                  },
                  "acknowledged_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "The caller's latest acceptance among them (may predate the handbook)."
                  }
                }
              },
              "sections": {
                "type": "array",
                "description": "The sections in order (`position` ascending). A policy section whose policy is no longer published, or that the caller may not read, is left out.",
                "items": {
                  "$ref": "#/components/schemas/PolicyHubHandbookSection"
                }
              }
            }
          }
        ]
      },
      "PolicyHubHandbookSection": {
        "type": "object",
        "required": [
          "type",
          "position",
          "title",
          "policy_id",
          "content"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "policy",
              "custom",
              "chapter_header",
              "divider"
            ],
            "description": "`policy` — a published policy (open `policy_id`); `custom` — the handbook's own text (`content`); `chapter_header` — a chapter title; `divider` — a visual break with no title or content."
          },
          "position": {
            "type": "integer",
            "description": "The section's place in the handbook (ascending)."
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "The section's own title, or for a `policy` section without one, the policy's title. Null for a `divider`."
          },
          "policy_id": {
            "type": "integer",
            "nullable": true,
            "description": "`policy` sections: the policy to open at `GET /policy_hub/policies/{policy_id}` (the body is not inlined). Other types do not show a policy — ignore it on them (normally null)."
          },
          "content": {
            "type": "string",
            "nullable": true,
            "description": "The text of a `custom` section; null for every other type."
          }
        }
      },
      "PracticeHubPaginationMeta": {
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total",
          "total_pages"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "description": "The page returned."
          },
          "per_page": {
            "type": "integer",
            "description": "Rows per page used."
          },
          "total": {
            "type": "integer",
            "description": "Rows across all pages."
          },
          "total_pages": {
            "type": "integer"
          }
        }
      },
      "PracticeHubScenario": {
        "type": "object",
        "required": [
          "id",
          "title",
          "category",
          "difficulty",
          "visibility",
          "official",
          "certifiable",
          "estimated_minutes",
          "average_rating",
          "attempts_count",
          "personas"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "Free-form snake_case topic (e.g. `customer_service`, `de_escalation`, `leadership`); not limited to a fixed list."
          },
          "difficulty": {
            "type": "string",
            "enum": [
              "beginner",
              "intermediate",
              "advanced"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "community",
              "curated",
              "official"
            ]
          },
          "official": {
            "type": "boolean",
            "description": "True when `visibility` is `official`."
          },
          "certifiable": {
            "type": "boolean",
            "description": "Whether passing the scenario can count toward a certification."
          },
          "estimated_minutes": {
            "type": "integer",
            "nullable": true
          },
          "average_rating": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Learners' average rating (two decimals); null before any rating."
          },
          "attempts_count": {
            "type": "integer"
          },
          "personas": {
            "type": "array",
            "description": "The AI characters the learner practises with, in their set order.",
            "items": {
              "type": "object",
              "required": [
                "name",
                "role"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "PracticeHubAttempt": {
        "type": "object",
        "required": [
          "id",
          "scenario_id",
          "scenario_title",
          "status",
          "passed",
          "score",
          "started_at",
          "completed_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "scenario_id": {
            "type": "integer"
          },
          "scenario_title": {
            "type": "string",
            "nullable": true,
            "description": "The scenario's title; null if the scenario can no longer be loaded."
          },
          "status": {
            "type": "string",
            "enum": [
              "in_progress",
              "completed",
              "scored",
              "abandoned"
            ]
          },
          "scoring": {
            "type": "string",
            "nullable": true,
            "enum": [
              "pending",
              "failed",
              null
            ],
            "description": "A finished roleplay's scoring run: `pending` while it is queued or running (\"Being scored…\" on the attempt page), `failed` when it gave up (\"Scoring failed\", with Try scoring again); null otherwise, and always null for a recording analysis."
          },
          "passed": {
            "type": "boolean",
            "nullable": true,
            "description": "Null until the attempt is scored."
          },
          "score": {
            "type": "integer",
            "nullable": true,
            "description": "The scorecard's final score — a reviewer's override when set, else the overall score; null when there is no scorecard or no score yet."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "RecentEntitiesEmployeeList": {
        "type": "object",
        "required": [
          "entity_type",
          "count",
          "entities"
        ],
        "properties": {
          "entity_type": {
            "type": "string",
            "enum": [
              "employee"
            ]
          },
          "count": {
            "type": "integer",
            "description": "How many entities are in this response."
          },
          "entities": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "position",
                "status",
                "updated_at"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "email": {
                  "type": "string",
                  "nullable": true,
                  "description": "Only for a browser session or a token holding `read:users` (`admin` and `read:all` count), and only where the People directory shows email to the caller; left out otherwise. Null for a person with no email address."
                },
                "position": {
                  "type": "string",
                  "nullable": true,
                  "description": "The person's role type, else their effective role, else \"Employee\"; null when the People directory hides Job Function from the caller."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "Active",
                    "Inactive"
                  ],
                  "description": "Active while the person has an active membership in this business on an active account; Inactive once they have left it."
                },
                "updated_at": {
                  "type": "string",
                  "description": "`YYYY-MM-DD HH:MM` in the caller's time zone.",
                  "example": "2026-10-09 14:05"
                }
              }
            }
          }
        }
      },
      "RecentEntitiesShiftList": {
        "type": "object",
        "required": [
          "entity_type",
          "count",
          "entities"
        ],
        "properties": {
          "entity_type": {
            "type": "string",
            "enum": [
              "shift"
            ]
          },
          "count": {
            "type": "integer",
            "description": "How many entities are in this response."
          },
          "entities": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "employee_name",
                "date",
                "time",
                "location",
                "status",
                "updated_at"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "employee_name": {
                  "type": "string",
                  "nullable": true,
                  "description": "The first assigned person's name; null when nobody is assigned."
                },
                "date": {
                  "type": "string",
                  "format": "date",
                  "description": "The shift's start date in its location's time zone."
                },
                "time": {
                  "type": "string",
                  "description": "`HH:MM - HH:MM` (start - end) in the shift location's time zone.",
                  "example": "09:00 - 17:00"
                },
                "location": {
                  "type": "string",
                  "nullable": true,
                  "description": "The location's name; null for a virtual location or none."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "scheduled",
                    "completed",
                    "cancelled",
                    "late_reported",
                    "absence_reported"
                  ]
                },
                "updated_at": {
                  "type": "string",
                  "description": "`YYYY-MM-DD HH:MM` in the caller's time zone.",
                  "example": "2026-10-09 14:05"
                }
              }
            }
          }
        }
      },
      "RecentEntitiesLocationList": {
        "type": "object",
        "required": [
          "entity_type",
          "count",
          "entities"
        ],
        "properties": {
          "entity_type": {
            "type": "string",
            "enum": [
              "location"
            ]
          },
          "count": {
            "type": "integer",
            "description": "How many entities are in this response."
          },
          "entities": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "address",
                "status",
                "updated_at"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "address": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "Active",
                    "Inactive"
                  ]
                },
                "updated_at": {
                  "type": "string",
                  "description": "`YYYY-MM-DD HH:MM` in the caller's time zone.",
                  "example": "2026-10-09 14:05"
                }
              }
            }
          }
        }
      },
      "RecipientGroupParametersOption": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "RecipientGroupParametersEmployee": {
        "type": "object",
        "required": [
          "id",
          "name",
          "email"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The user's id."
          },
          "name": {
            "type": "string",
            "description": "\"First Last\", trimmed; an empty string when both are blank. The legal first name — a preferred name is matched by `q` but not shown."
          },
          "email": {
            "type": "string",
            "nullable": true,
            "description": "Null for the many members who have no email address."
          }
        }
      },
      "RecipientGroupParametersRequiredParameter": {
        "type": "object",
        "required": [
          "type",
          "label",
          "required"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "location",
              "department",
              "affected_criteria"
            ],
            "description": "Which list fills it: `location` → /recipient_group_parameters/locations, `department` → /recipient_group_parameters/departments, `affected_criteria` → /recipient_group_parameters/employees."
          },
          "label": {
            "type": "string",
            "enum": [
              "Select Locations",
              "Select Departments",
              "Specify Affected Employees"
            ],
            "description": "The prompt to show beside the input, fixed per `type`."
          },
          "required": {
            "type": "boolean",
            "description": "Always true."
          }
        }
      },
      "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
              },
              "max_company_values": {
                "type": "integer",
                "description": "The most company values ONE give (or edit) may carry in `company_value_ids`.",
                "example": 3
              },
              "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 a company value when positive. Silently 0 when the tenant has peer points switched off.",
            "example": 250
          },
          "company_value_ids": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "integer"
            },
            "description": "The core values this recognition celebrates (each one of `config.values`), primary first — up to `limits.max_company_values`. At least one is REQUIRED once `points` is positive.",
            "example": [
              4,
              9
            ]
          },
          "company_value_id": {
            "type": "integer",
            "nullable": true,
            "description": "One core value — the list's one-value form, read when `company_value_ids` is absent.",
            "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,
            "description": "The main (first) company value's name.",
            "example": "Customer First"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "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,
            "description": "The main (first) company value's name.",
            "example": "Customer First"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "example": "Customer First"
                }
              }
            }
          },
          "can_share_outside": {
            "type": "boolean",
            "description": "Whether the caller may share this outside the company (`POST /moment_shares`) — only the person it is about, while it is public and the company's Shared moments switch is on, and only with a token holding write:recognition.",
            "example": false
          },
          "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"
          },
          "detail": {
            "type": "string",
            "description": "What tells this person apart from a colleague with the same name: their job title, then (only when two people in the list share a name) their location, department or Employee ID, joined by \" · \". Each fact only as far as the People directory shows it to the caller; absent when there is nothing to show.",
            "example": "Support Specialist · Denver"
          }
        }
      },
      "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": "Employee of the Quarter — Q3"
          },
          "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": "Employee of the Quarter — Q2 — 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."
          },
          "can_share_outside": {
            "type": "boolean",
            "description": "Whether the caller (the winner) may share this award outside the company (`POST /moment_shares`, subject_type `award`, subject_id `certificate.award_id`) — as the web results page offers them Share.",
            "example": false
          }
        }
      },
      "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": "Employee of the Quarter — Q2 — 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 Employee of the Quarter — Q2."
          },
          "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"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "example": "Customer 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 → one `company_value` chip per value the award celebrates, primary first), 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": "Employee of the Quarter"
                },
                "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"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "example": "Customer First"
                }
              }
            }
          },
          "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."
          },
          "justification": {
            "type": "string",
            "nullable": true,
            "description": "The nominator's justification — why this person deserves the award. Null when none was given. Distinct from `description`.",
            "example": "Saved a seven-figure renewal by rebuilding the rollout plan in a week."
          },
          "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 approval request for peer recognition held for manager approval. A group give's posts waiting on the same approver are ONE request — listed once, counted once, and decided together: approving or rejecting `id` decides every post in `post_ids`.",
        "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"
          },
          "recipients": {
            "type": "array",
            "description": "Everyone this request covers — one entry for a single recognition, each recipient of a group give who shares this approver otherwise.",
            "items": {
              "$ref": "#/components/schemas/RecognitionPerson"
            }
          },
          "post_ids": {
            "type": "array",
            "description": "The posts one decision on this request approves or rejects.",
            "items": {
              "type": "integer"
            },
            "example": [
              401,
              402,
              403
            ]
          },
          "points": {
            "type": "integer",
            "description": "The peer points attached to EACH post of the request (already in points; 0 when none). Approving a request that covers several posts settles this amount for every post in `post_ids`.",
            "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"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "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 job title alone, with no fallback, 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"
          },
          "recognized_via": {
            "type": "string",
            "enum": [
              "award",
              "acknowledgment"
            ],
            "nullable": true,
            "description": "How a `recognized` row was recognized, as the web row words it: `award` when a person gave the years-of-service award themselves (\"Award given\"), `acknowledgment` when a leader acknowledged the automatic award (\"Acknowledged\"). Null for `pending` and `rewarded` rows.",
            "example": "award"
          },
          "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"
          },
          "company_values": {
            "type": "array",
            "description": "Every company value it celebrates, primary first — up to three.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 4
                },
                "name": {
                  "type": "string",
                  "example": "Customer First"
                }
              }
            }
          },
          "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."
          }
        }
      },
      "RecorderRecording": {
        "type": "object",
        "description": "A recording. Lists answer the base fields; single-recording answers add the detail fields (`detail: true`). A field marked owner-only or editor-only is absent — not null — for anyone else.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "video",
              "screenshot"
            ]
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "uploading",
              "processing",
              "ready",
              "failed"
            ]
          },
          "processing_stage": {
            "type": "string",
            "nullable": true
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "company",
              "anyone"
            ]
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "client_capture_id": {
            "type": "string",
            "nullable": true
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "duration_seconds": {
            "type": "number",
            "nullable": true
          },
          "width": {
            "type": "integer",
            "nullable": true
          },
          "height": {
            "type": "integer",
            "nullable": true
          },
          "allow_download": {
            "type": "boolean"
          },
          "deleted": {
            "type": "boolean"
          },
          "days_until_purge": {
            "type": "integer",
            "nullable": true
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "web_url": {
            "type": "string",
            "nullable": true
          },
          "public_url": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "permission": {
            "type": "string",
            "nullable": true,
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          },
          "can_edit": {
            "type": "boolean"
          },
          "has_annotations": {
            "type": "boolean"
          },
          "can_delete": {
            "type": "boolean",
            "description": "The caller owns the recording and the token carries `destructive:recorder`."
          },
          "can_download": {
            "type": "boolean",
            "description": "Whether to offer a Download item. Not a gate on the media URL."
          },
          "owner": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecorderPerson"
              }
            ],
            "nullable": true
          },
          "cta_label": {
            "type": "string",
            "nullable": true
          },
          "cta_url": {
            "type": "string",
            "nullable": true
          },
          "comments_count": {
            "type": "integer",
            "description": "Live comments, replies included."
          },
          "cta_clicks_count": {
            "type": "integer",
            "description": "Owner-only."
          },
          "views_count": {
            "type": "integer",
            "description": "Owner-only."
          },
          "unique_viewers_count": {
            "type": "integer",
            "description": "Owner-only."
          },
          "names_hidden": {
            "type": "object",
            "description": "Owner-only, present while the workspace withholds who watched; the two play counters then count only the people who may be named.",
            "properties": {
              "count": {
                "type": "integer",
                "nullable": true
              },
              "group_count": {
                "type": "integer",
                "nullable": true
              },
              "fewer_than": {
                "type": "integer"
              },
              "reason": {
                "type": "string"
              }
            }
          },
          "detail": {
            "type": "boolean",
            "description": "Detail field — true on a single-recording answer."
          },
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Detail field — signed media URL (one hour) once the recording is ready."
          },
          "content_type": {
            "type": "string",
            "nullable": true,
            "description": "Detail field."
          },
          "byte_size": {
            "type": "integer",
            "nullable": true,
            "description": "Detail field."
          },
          "language": {
            "type": "string",
            "nullable": true,
            "description": "Detail field."
          },
          "summary_markdown": {
            "type": "string",
            "nullable": true,
            "description": "Detail field."
          },
          "chapters": {
            "type": "array",
            "description": "Detail field — sorted, within the recording's length.",
            "items": {
              "type": "object",
              "properties": {
                "start_seconds": {
                  "type": "integer"
                },
                "title": {
                  "type": "string"
                }
              }
            }
          },
          "has_transcript": {
            "type": "boolean",
            "description": "Detail field."
          },
          "caption_languages": {
            "type": "array",
            "description": "Detail field — the caption languages on offer.",
            "items": {
              "$ref": "#/components/schemas/RecorderCaptionLanguage"
            }
          },
          "shared_with": {
            "type": "array",
            "description": "Detail field, editor-only — the people it is shared with.",
            "items": {
              "type": "object",
              "properties": {
                "user_id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string",
                  "nullable": true
                },
                "permission": {
                  "type": "string"
                },
                "added_by": {
                  "type": "object",
                  "nullable": true,
                  "description": "Who added this person (as on `GET …/shares`); null for a share made before the adder was recorded, or whose adder's account was deleted.",
                  "properties": {
                    "user_id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "shared_with_rooms": {
            "type": "array",
            "description": "Detail field, editor-only (absent with `shared_with`) — the chat rooms that can watch it because it was sent to them.",
            "items": {
              "$ref": "#/components/schemas/RecorderSharedRoom"
            }
          },
          "can_manage_sharing": {
            "type": "boolean",
            "description": "Detail field, editor-only — may add a person or Stop a room (`DELETE …/chat_rooms/{room_id}`); false means those answer 403 `resharing_not_permitted`."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Detail field — the capture facts the client sent."
          },
          "annotations_url": {
            "type": "string",
            "nullable": true,
            "description": "Detail field — signed URL (one hour) of the annotations bundle, for someone who may edit."
          }
        }
      },
      "RecorderSharedRoom": {
        "type": "object",
        "properties": {
          "room_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "The room as this reader may see it: its name, the other person of a direct message, or \"A private chat room\" / \"A direct message\" where Chat keeps it from them."
          },
          "sent_by": {
            "type": "string",
            "nullable": true,
            "description": "Who sent the recording there; null when unknown."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "RecorderRecycleBinRecording": {
        "type": "object",
        "description": "A recording in the recycle bin as shown to someone who may restore it but not watch it (a Recorder admin) — no media, poster or description.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "video",
              "screenshot"
            ]
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "uploading",
              "processing",
              "ready",
              "failed"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "company",
              "anyone"
            ]
          },
          "deleted": {
            "type": "boolean"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "days_until_purge": {
            "type": "integer",
            "nullable": true
          },
          "owner": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecorderPerson"
              }
            ],
            "nullable": true
          },
          "permission": {
            "type": "string",
            "nullable": true,
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          },
          "can_edit": {
            "type": "boolean"
          },
          "can_delete": {
            "type": "boolean"
          }
        }
      },
      "RecorderCreatedRecording": {
        "type": "object",
        "required": [
          "recording",
          "existing"
        ],
        "properties": {
          "recording": {
            "$ref": "#/components/schemas/RecorderRecording"
          },
          "existing": {
            "type": "boolean",
            "description": "True when an existing recording with this `client_capture_id` was returned."
          }
        }
      },
      "RecorderPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Full name, or email when there is no name."
          }
        }
      },
      "RecorderCaptionLanguage": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "description": "Language code; pass it as `lang` to `GET /recorder/recordings/{id}/captions`."
          },
          "label": {
            "type": "string"
          },
          "source": {
            "type": "boolean",
            "description": "The recording's own language (its transcript)."
          }
        }
      },
      "RecorderTranscriptSegment": {
        "type": "object",
        "properties": {
          "start": {
            "type": "number",
            "description": "Seconds."
          },
          "end": {
            "type": "number",
            "description": "Seconds."
          },
          "text": {
            "type": "string"
          }
        }
      },
      "RecorderMediaType": {
        "type": "string",
        "enum": [
          "video/webm",
          "video/mp4",
          "video/quicktime",
          "image/png",
          "image/jpeg"
        ]
      },
      "RecorderUploadLimits": {
        "type": "object",
        "properties": {
          "max_bytes": {
            "type": "integer",
            "description": "The workspace's upload cap."
          },
          "allowed_types": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecorderMediaType"
            }
          }
        }
      },
      "RecorderPreferences": {
        "type": "object",
        "properties": {
          "default_visibility": {
            "type": "string",
            "enum": [
              "private",
              "company",
              "anyone"
            ],
            "description": "The audience a new capture gets now — the caller's own choice when the workspace allows it, else the workspace default."
          },
          "own_default_visibility": {
            "type": "string",
            "nullable": true,
            "enum": [
              "private",
              "company",
              "anyone"
            ],
            "description": "The caller's own stored choice; null when none, or when the workspace no longer allows it."
          },
          "default_transcribe": {
            "type": "boolean"
          },
          "visibility_enforced": {
            "type": "boolean",
            "description": "The workspace default applies to everyone; a personal choice waits."
          }
        }
      },
      "RecorderConfig": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "example": true
          },
          "settings": {
            "type": "object",
            "properties": {
              "allow_downloads": {
                "type": "boolean"
              },
              "max_upload_size_mb": {
                "type": "integer"
              },
              "default_visibility": {
                "type": "string",
                "enum": [
                  "private",
                  "company",
                  "anyone"
                ]
              },
              "public_links_enabled": {
                "type": "boolean"
              },
              "public_link_max_days": {
                "type": "integer",
                "description": "Longest a public link may last, in days; 0 means no limit."
              },
              "transcription_enabled": {
                "type": "boolean"
              },
              "ai_summary_enabled": {
                "type": "boolean"
              }
            }
          },
          "preferences": {
            "$ref": "#/components/schemas/RecorderPreferences"
          },
          "capabilities": {
            "type": "object",
            "description": "What this caller, with this token, may do.",
            "properties": {
              "upload": {
                "type": "boolean"
              },
              "share": {
                "type": "boolean"
              },
              "public_links": {
                "type": "boolean",
                "description": "The token may write, public links are on, and the caller may share externally."
              },
              "send_to_chat": {
                "type": "boolean",
                "description": "The token carries `write:recorder` and `write:chat`, and Chat is open to the caller."
              },
              "handoffs": {
                "type": "array",
                "description": "The \"Use this recording\" hand-offs the caller may start.",
                "items": {
                  "type": "string",
                  "enum": [
                    "training",
                    "sop",
                    "library",
                    "ticket",
                    "bug",
                    "practice"
                  ]
                }
              }
            }
          }
        }
      },
      "RecorderPusherAuthRequest": {
        "type": "object",
        "required": [
          "socket_id",
          "channel_name"
        ],
        "properties": {
          "socket_id": {
            "type": "string",
            "description": "The Pusher socket id (`<digits>.<digits>`)."
          },
          "channel_name": {
            "type": "string",
            "description": "`private-user-recorder-{userId}-business-{businessId}`, with the caller's own ids."
          }
        }
      },
      "RecorderSentToChat": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "duplicate"
            ]
          },
          "recording_id": {
            "type": "integer"
          },
          "room_id": {
            "type": "integer"
          },
          "message_id": {
            "type": "integer",
            "nullable": true,
            "description": "The message already posted (`duplicate`); null while `queued`."
          },
          "link": {
            "type": "string",
            "enum": [
              "company",
              "anyone"
            ]
          },
          "url": {
            "type": "string",
            "description": "The link the card carries."
          }
        }
      },
      "RecorderShareLink": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "token": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "embed_url": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "auto": {
            "type": "boolean",
            "description": "Whether this is the recording's automatic \"anyone with the link\" link."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "password": {
            "type": "boolean",
            "description": "Whether the link asks for a password."
          },
          "require_email": {
            "type": "boolean"
          },
          "allow_download": {
            "type": "boolean",
            "description": "Whether a download works through this link now (the link's own switch and the recording's)."
          },
          "max_views": {
            "type": "integer",
            "nullable": true
          },
          "view_count": {
            "type": "integer"
          },
          "last_viewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "Active",
              "Expired",
              "View limit reached",
              "Revoked"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RecorderShare": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The share's id (`share_id`)."
          },
          "user_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "permission": {
            "type": "string",
            "enum": [
              "viewer",
              "editor"
            ]
          },
          "added_by": {
            "type": "object",
            "nullable": true,
            "description": "Who added this person to the roster. Null for a share made before the adder was recorded, or whose adder's account was deleted. A permission change keeps the original adder.",
            "properties": {
              "user_id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "RecorderShareRoster": {
        "type": "object",
        "required": [
          "recording_id",
          "shares",
          "total"
        ],
        "properties": {
          "recording_id": {
            "type": "integer"
          },
          "shares": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecorderShare"
            }
          },
          "total": {
            "type": "integer",
            "description": "People it is shared with, across every page."
          },
          "next_cursor": {
            "type": "integer",
            "nullable": true,
            "description": "The `cursor` (an offset) for the next page of `GET …/shares`; null on the last page."
          }
        }
      },
      "RecorderShareRosterAfterShare": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecorderShareRoster"
          },
          {
            "type": "object",
            "required": [
              "existing"
            ],
            "properties": {
              "existing": {
                "type": "boolean",
                "description": "True when the person was already on the list."
              }
            }
          }
        ]
      },
      "RecorderComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "body": {
            "type": "string",
            "description": "The text, with @mentions shown as names."
          },
          "body_html": {
            "type": "string",
            "description": "The text as HTML, with @mentions rendered."
          },
          "anchor_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "The moment in the recording it is pinned to."
          },
          "parent_comment_id": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "author": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RecorderPerson"
              }
            ],
            "nullable": true
          },
          "can_delete": {
            "type": "boolean",
            "description": "The caller wrote it or owns the recording, the recording is out of the recycle bin, and the token carries `destructive:recorder`."
          },
          "reply_count": {
            "type": "integer",
            "description": "Live replies under it — may be more than `replies` holds."
          },
          "replies_truncated": {
            "type": "boolean",
            "description": "True when `replies` holds fewer than `reply_count`."
          },
          "replies": {
            "type": "array",
            "description": "Up to 50 replies, oldest first (a reply's own `replies` is always empty).",
            "items": {
              "$ref": "#/components/schemas/RecorderComment"
            }
          }
        }
      },
      "RecruitingCandidateSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "first_name": {
            "type": "string",
            "nullable": true
          },
          "last_name": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "linkedin_url": {
            "type": "string",
            "nullable": true
          },
          "current_title": {
            "type": "string",
            "nullable": true
          },
          "current_company": {
            "type": "string",
            "nullable": true
          },
          "profile_picture_url": {
            "type": "string",
            "nullable": true
          },
          "job": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true
              },
              "title": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "profile_url_in_ats": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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 marked reviewed (or, if never reviewed, its question or answer text not changed) 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
          },
          "outcome": {
            "type": "string",
            "nullable": true,
            "enum": [
              "won",
              "lost",
              null
            ],
            "description": "How a submitted bid ended, as recorded in RFP Desk or by its proposal; null otherwise."
          },
          "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
              },
              "approval_blocker": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "no_questions",
                  "input",
                  "review",
                  "specialist_review",
                  "deliverables",
                  null
                ],
                "description": "The first thing that still stands between the response and approval, in the order the approval gate checks it — no questions yet, questions still needing input, sections unreviewed, a specialist sign-off, required checklist items not ticked. Null when nothing does, or once the response is approved, submitted or declined."
              },
              "required_items_outstanding": {
                "type": "array",
                "description": "Labels of required submission-checklist items nobody has ticked yet.",
                "items": {
                  "type": "string"
                }
              },
              "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
          },
          "draft_version": {
            "type": "string",
            "description": "Digest of the answer's words — send it back as `base_draft_version` when answering the section."
          },
          "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"
          },
          "owner_active": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the section's owner is still an active member of the workspace. False when they have left — the section keeps their name until someone sends it on (RFP Desk hands nothing over at a departure). Null when the section has no owner."
          },
          "waiting_on": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RfpPerson"
              }
            ],
            "nullable": true,
            "description": "Who an open question is with — the person its open routed task is assigned to. Differs from `owner` when the task was handed on in Tasks to someone the bid's routing rule did not let become its owner; that person, not the owner, is answering it. Null when the section is answered or no task is open for it."
          },
          "owed_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RfpPerson"
              }
            ],
            "nullable": true,
            "description": "Who owes an open question now, by the rule the bid page, the phone and the deadline reminder use — the holder of its open routed task, else its owner. Null when the section is answered, has no owner, or its request was withdrawn or declined and not sent again (nobody is waiting on it)."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "reused_library_answer": {
            "type": "string",
            "nullable": true,
            "enum": [
              "stale",
              "archived",
              null
            ],
            "description": "The library answer this section reuses is past the workspace's review window (`stale`) or has since been archived (`archived` — rewrite the section before sending). Null when neither."
          }
        }
      },
      "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",
            "nullable": true,
            "description": "Email of the person who ran it (null when they have no email address)"
          },
          "run_by_user_id": {
            "type": "integer",
            "nullable": true,
            "description": "User id 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"
                }
              }
            }
          }
        ]
      },
      "SearchFilters": {
        "type": "object",
        "description": "The filters applied, in the page's own params (`when` is echoed as `since`).",
        "properties": {
          "in": {
            "type": "string",
            "example": "policy_hub"
          },
          "since": {
            "type": "string",
            "enum": [
              "30d",
              "90d",
              "year"
            ]
          },
          "mine": {
            "type": "string",
            "enum": [
              "1"
            ]
          }
        }
      },
      "SearchFilterOption": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "nullable": true,
            "description": "The value to send; null means \"no filter\" (Everywhere / Any time)."
          },
          "title": {
            "type": "string"
          },
          "selected": {
            "type": "boolean"
          },
          "category": {
            "type": "object",
            "description": "In list only: the area's group (Search::Areas — the grouping Settings › What's in results uses). Absent on Everywhere and on an area no group claims.",
            "properties": {
              "key": {
                "type": "string",
                "enum": [
                  "people",
                  "content",
                  "work",
                  "comms",
                  "platform"
                ]
              },
              "title": {
                "type": "string",
                "example": "Content & knowledge"
              }
            }
          },
          "count": {
            "type": "integer",
            "description": "In list only: what picking this area would show — its matches under the current When and Only mine, before In (0 when it found nothing). On Everywhere, every match including external sources. Absent when unknown: under an In filter the engine ran that one area only."
          }
        }
      },
      "SearchSection": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "example": "policy_hub"
          },
          "title": {
            "type": "string",
            "example": "Policies"
          },
          "count": {
            "type": "integer",
            "description": "Rows in this section (`items` holds at most `limit` of them)."
          },
          "total": {
            "type": "integer",
            "description": "The area's full match count, only when it capped its rows (\"8 of 46\"). On `home.for_you`, the full number owed (policies to acknowledge, tasks due) when the home shows only the first few."
          },
          "see_all_url": {
            "type": "string",
            "description": "The app's own search for this query, where it has one. On `home.for_you`, the app's own list of everything owed (Policy Hub's pending acknowledgments, My Tasks, open shifts)."
          },
          "see_all_title": {
            "type": "string"
          },
          "also_in": {
            "type": "boolean",
            "description": "True when the page folds this section into its \"Also in\" block — a late section (after the first six) holding one row, when at least five such sections exist. People never fold."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchRow"
            }
          }
        }
      },
      "SearchRow": {
        "type": "object",
        "description": "One result. Fields a row has no value for are omitted.",
        "properties": {
          "id": {
            "description": "The record's id within its area (people rows carry the user id).",
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ]
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "subtitle": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "section": {
            "type": "string",
            "description": "The area a ranked row (`best_match`, `top`) came from."
          },
          "section_title": {
            "type": "string"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "owner": {
            "type": "string"
          },
          "owner_id": {
            "type": "integer"
          },
          "version": {
            "type": "string"
          },
          "event_dated": {
            "type": "boolean",
            "description": "True when `updated_at` is when the thing happens (a shift's start), not when it was last edited."
          },
          "outdated": {
            "type": "boolean"
          },
          "takes_effect_on": {
            "type": "string",
            "format": "date"
          },
          "semantic": {
            "type": "boolean",
            "description": "Found by meaning rather than by its words."
          },
          "promoted": {
            "type": "boolean",
            "description": "An admin-promoted result for this query."
          },
          "external_host": {
            "type": "string"
          },
          "snippet": {
            "type": "string",
            "description": "An external source's excerpt."
          },
          "passage": {
            "type": "string",
            "maxLength": 320,
            "description": "Where the row matched, as the results page draws it under the row — the sentence a Messages result's word is in (its subject, for a thread matched on it), a page's matched paragraph, or the record's summary (a summary's markdown is stripped). Text to draw as-is, never HTML; at most 320 characters."
          },
          "passage_term": {
            "type": "string",
            "description": "The words to mark in `passage` (case-insensitive), as the web marks them. Sent only with a passage, and only when it is at least two characters."
          },
          "score": {
            "type": "number"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "applies_chip": {
            "type": "string",
            "description": "A policy row's applicability chip, e.g. \"Applies to you · Sales\" or \"For California employees\"."
          },
          "status": {
            "type": "string",
            "description": "What the caller already did with the row, in place of its action — \"Pick-up requested · pending approval\", \"You're registered\", \"On the waitlist\" or \"You attended\". Absent otherwise."
          },
          "actions": {
            "type": "array",
            "description": "What the caller can do from the row (e.g. acknowledge, open). Each is `{label, url, method, primary, confirm}`. A `get` action is a page to open. A `post` action is the web app's own form endpoint: it needs a signed-in browser session and its CSRF token, not a bearer token — open the row's own `url` in a web view, or call that app's own API for the action.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "person": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SearchPersonCard"
              }
            ],
            "description": "On every People row — in the People section and as the best match or a top row."
          }
        }
      },
      "SearchPersonCard": {
        "type": "object",
        "description": "A person's card facts, on rows in the `people` section and on experts — loaded for the whole page at once. Fields the viewer may not see are omitted.",
        "properties": {
          "job_title": {
            "type": "string"
          },
          "department": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "manager_name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "presence": {
            "type": "string",
            "description": "On shift / clocked in, when known."
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "disambiguator": {
            "type": "string",
            "description": "Set only when another card on the page reads identically."
          },
          "deactivated": {
            "type": "boolean"
          },
          "booking_url": {
            "type": "string",
            "description": "The person's public booking page, when they have one."
          }
        }
      },
      "SearchSavedSearch": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "query": {
            "type": "string"
          },
          "filters": {
            "$ref": "#/components/schemas/SearchFilters"
          },
          "alerting": {
            "type": "boolean"
          },
          "alert_cadence": {
            "type": "string",
            "enum": [
              "off",
              "daily",
              "weekly"
            ]
          },
          "cadence_label": {
            "type": "string",
            "example": "Weekly"
          },
          "last_alerted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the last alert went out. Absent until one has (a search saved with alerts off never has one)."
          },
          "last_match_count": {
            "type": "integer",
            "description": "The new results that last alert reported (\"4 new\"). Only alongside `last_alerted_at`.",
            "example": 4
          },
          "last_checked_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the alert pass last reached this search, whether or not it found any — \"Checked 2 hours ago · nothing new yet\". A daily alert checked days ago is not running. A pass also stamps it when it skips a muted alert, so read it beside `alerts_muted`."
          },
          "alerts_muted": {
            "type": "string",
            "enum": [
              "mine",
              "organisation"
            ],
            "description": "Present on an alerting search whose alerts will not be delivered — `mine` when the person muted Search notices in their notification settings, `organisation` when the organisation's settings turn them off."
          },
          "area_closed": {
            "type": "string",
            "enum": [
              "excluded",
              "no_access"
            ],
            "description": "Present on an alerting search narrowed (`filters.in`) to an area the organisation switched off in Search settings (`excluded`) or whose app the person can no longer open (`no_access`); such a search finds nothing on any pass, so it is not checked."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TicketActivityEntry": {
        "type": "object",
        "description": "A comment (type \"comment\", with the TicketComment fields) or a lifecycle milestone (type \"milestone\").",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "comment",
              "milestone"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "description": "Milestones only — submitted, assigned, priority_changed, resolved, …"
          },
          "headline": {
            "type": "string",
            "description": "Milestones only — the line as this caller may read it"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "icon": {
            "type": "string",
            "description": "Milestones only — a Font Awesome class"
          },
          "staff_only": {
            "type": "boolean"
          },
          "content": {
            "type": "string",
            "description": "Comments only"
          },
          "internal": {
            "type": "boolean",
            "description": "Comments only"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceDeskAttachment"
            }
          }
        }
      },
      "ServiceDeskAttachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "Absolute download URL; expires after one hour, so fetch it again from the ticket rather than storing it."
          }
        }
      },
      "TicketBillingProject": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "label": {
            "type": "string",
            "description": "Code and name together, as the web shows it"
          }
        }
      },
      "TicketPermissions": {
        "type": "object",
        "description": "What the caller may do on this ticket right now (role and ticket state together) — the same rules the web ticket page draws its buttons from. Draw a control only when its flag is true.",
        "properties": {
          "can_edit": {
            "type": "boolean"
          },
          "can_assign": {
            "type": "boolean",
            "description": "Assign or transfer (POST /api/v1/service_desk/{id}/assign) — false on a closed, cancelled, rejected or pending-approval ticket"
          },
          "can_claim": {
            "type": "boolean",
            "description": "Take this unowned ticket yourself (POST /api/v1/service_desk/{id}/claim) — Service Desk staff, from a queue they are on (admins from any), never their own ticket under prevent-self-service"
          },
          "can_comment": {
            "type": "boolean",
            "description": "May post a comment the requester can see (POST /api/v1/service_desk/{id}/comments) — false on a rejected or cancelled ticket, and for a token without write:service_desk"
          },
          "can_comment_internal": {
            "type": "boolean",
            "description": "May post a comment with `internal: true` (staff or the assignee) — false on a rejected or cancelled ticket, and for a token without write:service_desk"
          },
          "can_approve": {
            "type": "boolean"
          },
          "can_reject": {
            "type": "boolean"
          },
          "can_cancel": {
            "type": "boolean"
          },
          "can_reinstate": {
            "type": "boolean"
          },
          "can_rate": {
            "type": "boolean",
            "description": "The requester may submit a satisfaction rating (once, after the ticket is resolved or closed)"
          },
          "can_escalate": {
            "type": "boolean"
          },
          "can_de_escalate": {
            "type": "boolean"
          },
          "can_merge": {
            "type": "boolean",
            "description": "Merge this ticket into another (POST /api/v1/service_desk/{id}/merge) — false once it is merged, cancelled, rejected, resolved or closed"
          },
          "can_clone": {
            "type": "boolean",
            "description": "Start a copy (GET /api/v1/service_desk/{id}/clone) — never on a grievance"
          },
          "can_log_time": {
            "type": "boolean"
          },
          "can_manage_parts": {
            "type": "boolean",
            "description": "May request, cancel, dismiss and return parts (staff, with the Procurement app on and open to them)"
          },
          "can_view_participants": {
            "type": "boolean"
          },
          "can_add_participants": {
            "type": "boolean"
          },
          "allowed_status_actions": {
            "type": "array",
            "description": "The `action_type` values PATCH /api/v1/service_desk/{id}/status will accept from this caller now.",
            "items": {
              "type": "string",
              "enum": [
                "start_progress",
                "revert_to_assigned",
                "resolve",
                "close",
                "reopen",
                "approve",
                "reject",
                "cancel",
                "reinstate"
              ]
            }
          }
        }
      },
      "ServiceDeskIntakeForm": {
        "type": "object",
        "description": "Form-wide New Request rules for this tenant and viewer.",
        "properties": {
          "priority": {
            "type": "object",
            "properties": {
              "shown": {
                "type": "boolean",
                "description": "Draw the priority picker. When false, leave priority out: either the\ntenant sets it (levels off, system / agent mode) or it is hidden from\nrequesters.\n"
              },
              "default": {
                "type": "string",
                "nullable": true,
                "example": "medium"
              },
              "hidden_note": {
                "type": "string",
                "nullable": true,
                "description": "Sentence to show in place of the picker when it is hidden."
              },
              "urgency_reason_required_for": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Priorities that need a \"Why is this urgent?\" answer. Empty when the picker is hidden.",
                "example": [
                  "high",
                  "critical"
                ]
              }
            }
          },
          "approval_cost_threshold": {
            "type": "number",
            "nullable": true,
            "description": "Show \"Amounts over <this> require approval\" under Estimated cost — a higher cost sends the request for approval.",
            "example": 500.0
          },
          "anonymous_allowed": {
            "type": "boolean",
            "description": "Offer the \"Submit anonymously\" switch."
          },
          "kb": {
            "type": "object",
            "properties": {
              "suggest": {
                "type": "boolean",
                "description": "Suggest knowledge-base articles while the request is typed (`GET /service_desk/kb/search`)."
              },
              "require_acknowledgement": {
                "type": "boolean",
                "description": "Ask the requester to confirm the suggestions did not help before submitting."
              }
            }
          }
        }
      },
      "ServiceDeskIntakeTypeRules": {
        "type": "object",
        "description": "One request type's New Request rules. Present on `service_types` entries only.",
        "properties": {
          "description_required": {
            "type": "boolean"
          },
          "requires_approval": {
            "type": "boolean",
            "description": "The type always needs approval. A high `estimated_cost` can also require it."
          },
          "title_template": {
            "type": "string",
            "nullable": true,
            "example": "Equipment Request: [Item Name]"
          },
          "description_template": {
            "type": "string",
            "nullable": true
          },
          "fields": {
            "type": "object",
            "description": "Visibility and required rules for the optional layout fields.",
            "properties": {
              "location": {
                "$ref": "#/components/schemas/ServiceDeskIntakeFieldRule"
              },
              "due_at": {
                "$ref": "#/components/schemas/ServiceDeskIntakeFieldRule"
              },
              "estimated_cost": {
                "$ref": "#/components/schemas/ServiceDeskIntakeFieldRule"
              },
              "business_justification": {
                "$ref": "#/components/schemas/ServiceDeskIntakeFieldRule"
              }
            }
          },
          "custom_fields": {
            "type": "array",
            "description": "The type's custom fields in the admin's layout order.",
            "items": {
              "$ref": "#/components/schemas/ServiceDeskCustomFieldDefinition"
            }
          }
        }
      },
      "ServiceDeskIntakeFieldRule": {
        "type": "object",
        "properties": {
          "visible": {
            "type": "boolean"
          },
          "required": {
            "type": "boolean"
          }
        }
      },
      "ServiceDeskCustomFieldDefinition": {
        "type": "object",
        "description": "The same definition the web form draws from.",
        "properties": {
          "field_name": {
            "type": "string",
            "description": "The key to answer it under in `ticket.custom_fields`."
          },
          "label": {
            "type": "string"
          },
          "field_type": {
            "type": "string",
            "enum": [
              "text",
              "textarea",
              "number",
              "email",
              "url",
              "select",
              "multiselect",
              "radio",
              "checkbox",
              "date",
              "info_block"
            ],
            "description": "`info_block` is display-only text."
          },
          "required": {
            "type": "boolean"
          },
          "placement": {
            "type": "string",
            "enum": [
              "before_title",
              "after_title"
            ]
          },
          "options": {
            "type": "object",
            "additionalProperties": true,
            "description": "`choices` (array of strings) for select / multiselect / radio, plus\noptional `visibility_condition` / `required_condition` rules the web\nform evaluates on the client.\n"
          },
          "placeholder": {
            "type": "string",
            "nullable": true
          },
          "help_text": {
            "type": "string",
            "nullable": true
          },
          "validation_rules": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "dynamic_source": {
            "type": "string",
            "nullable": true,
            "description": "`mangoapps_system_user` / `_location` / `_department`: choose from that directory and send the display name."
          },
          "dynamic_source_searchable": {
            "type": "boolean",
            "description": "The choices are not listed (people search) — look the person up and send their name."
          },
          "internal_only": {
            "type": "boolean",
            "description": "Staff-only field; never sent to a requester."
          }
        }
      },
      "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": "Display label in the language the client asks for (`locale`, else `Accept-Language`, else the caller's own language). Display only — branch on `key`.",
            "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"
          }
        }
      },
      "ClockInBrief": {
        "type": "object",
        "description": "\"Before you start\" — `GET /attendance_records/{id}/clock_in_brief`.\n`since` is the caller's last clock-out before this punch (at most 14\ndays back; 7 when they have never clocked out). `degraded` names any\npart that failed to load, so a short brief is never read as complete.\n\nSince Knowledge Layer E2 the brief carries the ONE \"Waiting on you\" (the\nsame list as `waiting_on_you` on `GET /home`): `waiting_on_you.total`,\nthen its two halves — `to_decide` and `owed` (\"To acknowledge\"). At most\nfive items show across both, \"To decide\" first; \"See all N\" is the\nInbox's Waiting on you. `work` (\"Your tasks\") no longer repeats either\nhalf, and `before_you_start` is the place's own message for this shift.\n",
        "properties": {
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "last_clock_out_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The caller's real last clock-out before this punch — null on a\nfirst-ever punch, and earlier than `since` when the window was\ncapped at 14 days. Say \"since you last clocked out\" only when it\nequals `since`; otherwise the window is \"the last N days\".\n"
          },
          "waiting_on_you": {
            "type": "object",
            "description": "The whole of Waiting on you — `to_decide.total` plus `owed.total`.",
            "properties": {
              "total": {
                "type": "integer"
              }
            }
          },
          "to_decide": {
            "type": "object",
            "description": "\"To decide\" — what the caller decides (the approvals registry:\npeople's requests, AI suggestions, …). `items` is the first up to 5,\nmost urgent first; `total` and `counts_by_source` the whole set.\n",
            "properties": {
              "total": {
                "type": "integer"
              },
              "counts_by_source": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "items": {
                "type": "array",
                "maxItems": 5,
                "items": {
                  "type": "object",
                  "description": "One \"To decide\" row of Waiting on you (Knowledge Layer E2) — a request\nor suggestion whose decision is the caller's, from the approvals\nregistry. Decided with `POST /api/v1/home/waiting_on_you/decide`\n(`source`, `id`; Knowledge Layer C3) or where `url` links;\n`can_approve` / `can_decline` say which verbs will be taken right now —\nfrom an API token, what that decide door takes from it: neither without\n`write:approvals`, and no delete-level verdict without its area's delete\nscope (`GET /home` and the clock-in brief alike).\n",
                  "properties": {
                    "source": {
                      "type": "string",
                      "description": "The approvals source (queue) key.",
                      "example": "leave_requests"
                    },
                    "id": {
                      "type": "integer",
                      "description": "The record's id in that source."
                    },
                    "kind_label": {
                      "type": "string",
                      "example": "Time off"
                    },
                    "title": {
                      "type": "string",
                      "description": "Subject and summary as one line.",
                      "example": "Ana Morales — Vacation · 9 Oct – 10 Oct"
                    },
                    "subtitle": {
                      "type": "string",
                      "nullable": true
                    },
                    "requested_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "urgency": {
                      "type": "string",
                      "enum": [
                        "overdue",
                        "soon",
                        "normal"
                      ]
                    },
                    "urgency_label": {
                      "type": "string",
                      "nullable": true,
                      "description": "The words for `urgency` (\"Overdue\", \"Due soon\", or the source's own); null for `normal`."
                    },
                    "approve_label": {
                      "type": "string",
                      "example": "Approve"
                    },
                    "decline_label": {
                      "type": "string",
                      "example": "Decline"
                    },
                    "can_approve": {
                      "type": "boolean"
                    },
                    "can_decline": {
                      "type": "boolean"
                    },
                    "decline_needs_reason": {
                      "type": "boolean"
                    },
                    "approve_question": {
                      "type": "string",
                      "nullable": true,
                      "description": "A question to ask the person before sending `approve` (for example when they are approving their own request); null when Approve asks nothing."
                    },
                    "decline_question": {
                      "type": "string",
                      "nullable": true,
                      "description": "A question to ask before sending `decline` (a refusal that takes no reason and cannot be undone); null when Decline asks nothing."
                    },
                    "url": {
                      "type": "string",
                      "nullable": true,
                      "description": "Where it is decided (the /m/ page on the API)."
                    }
                  }
                }
              }
            }
          },
          "owed": {
            "type": "object",
            "description": "\"To acknowledge\" — everything waiting on the caller's confirmation\n(every kind). `items` fills what `to_decide` leaves of the five shown\n(overdue first); `total` and `counts_by_kind` are the whole set — the\nrest is the Inbox's Waiting on you.\n",
            "properties": {
              "total": {
                "type": "integer"
              },
              "counts_by_kind": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string",
                      "example": "must_read_post"
                    },
                    "id": {
                      "type": "integer"
                    },
                    "title": {
                      "type": "string"
                    },
                    "kind_label": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "description": "The /m/ page where it is read and confirmed."
                    },
                    "asked_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "due_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "overdue": {
                      "type": "boolean"
                    },
                    "action_label": {
                      "type": "string"
                    },
                    "open_label": {
                      "type": "string",
                      "description": "The words for a button that opens the item instead (\"Read and acknowledge\" for a must-read post or broadcast)."
                    },
                    "confirm_in_chat": {
                      "type": "boolean",
                      "description": "True when it can be confirmed in place (`POST …/clock_in_brief/confirm`)."
                    }
                  }
                }
              }
            }
          },
          "shift_changes": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "description": "Soonest shift first, at most 10.",
                "items": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "added",
                        "changed",
                        "cancelled",
                        "removed"
                      ]
                    },
                    "label": {
                      "type": "string",
                      "example": "Time changed"
                    },
                    "shift_id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "starts_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ends_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "time_zone": {
                      "type": "string"
                    },
                    "location": {
                      "type": "string",
                      "nullable": true
                    },
                    "changed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "when": {
                      "type": "string",
                      "example": "Tue 30 Sep, 9:00 AM – 5:00 PM"
                    },
                    "was": {
                      "type": "string",
                      "nullable": true,
                      "example": "was 8:00 AM – 4:00 PM"
                    }
                  }
                }
              }
            }
          },
          "work": {
            "type": "object",
            "nullable": true,
            "description": "\"Your tasks\" — My Day's one list less its must-reads and approvals\n(both are in Waiting on you); `items` is the top 5.\n",
            "properties": {
              "total": {
                "type": "integer"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "example": "tasks"
                    },
                    "title": {
                      "type": "string"
                    },
                    "subtitle": {
                      "type": "string",
                      "nullable": true
                    },
                    "due_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "overdue": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "action_label": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "news": {
            "type": "object",
            "description": "What arrived since `since` (counts only).",
            "properties": {
              "feed": {
                "type": "integer"
              },
              "messages": {
                "type": "integer"
              },
              "chat": {
                "type": "integer",
                "description": "Chat rooms with unread messages since `since`."
              },
              "mango_cs": {
                "type": "integer"
              },
              "mango_cs_news": {
                "type": "integer"
              },
              "recognition": {
                "type": "integer"
              },
              "summary": {
                "type": "string",
                "nullable": true,
                "example": "4 new posts and 2 conversations with new messages"
              }
            }
          },
          "before_you_start": {
            "type": "object",
            "nullable": true,
            "description": "The place's own \"Before you start\" message for this shift (the\npost-clock-in Comms placement), part of the brief rather than a\nsecond card. Null when there is none, or no open punch.\n",
            "properties": {
              "placement_id": {
                "type": "integer"
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "from": {
                "type": "string",
                "nullable": true,
                "description": "The sender's name."
              },
              "requires_acknowledgment": {
                "type": "boolean"
              }
            }
          },
          "degraded": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CommsPlacement": {
        "type": "object",
        "description": "A placement on the one Communications placement engine — a message\nbooked into a place (a shift, Top of Home, a store's or department's\npin). Read-only: open the message with `GET /broadcasts/{id}` or\n`GET /feeds/{id}` (`message_type` + `broadcast_id` / `feed_id` say\nwhich), acknowledge it through that message's own acknowledge endpoint,\ndismiss it with `POST /broadcasts/{id}/placements/{placement_id}/dismiss`\nor `POST /feeds/{id}/placements/{placement_id}/dismiss`.\n\nSince Knowledge Layer Phase 3 (E7) a placement may hold a feed post, its\nwindow is a datetime window (`starts_at` / `ends_at`; `starts_on` /\n`ends_on` stay for older clients), and `pinned_label` is the line Home\nshows above it.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "message_type": {
            "type": "string",
            "enum": [
              "broadcast",
              "feed"
            ]
          },
          "broadcast_id": {
            "type": "integer",
            "nullable": true
          },
          "feed_id": {
            "type": "integer",
            "nullable": true
          },
          "slot_key": {
            "type": "string",
            "description": "The place it is booked into.",
            "example": "shifts.shift_detail"
          },
          "slot_label": {
            "type": "string",
            "nullable": true
          },
          "kicker": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "The message's title (a broadcast in the reader's language)."
          },
          "body": {
            "type": "string",
            "description": "Its text below the title, as plain text."
          },
          "cta_label": {
            "type": "string",
            "description": "The button's words (the placement's own, else the default)."
          },
          "requires_acknowledgment": {
            "type": "boolean"
          },
          "acknowledged": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the caller acknowledged it; null when it asks for none."
          },
          "sender_name": {
            "type": "string",
            "example": "Communications"
          },
          "pinned_label": {
            "type": "string",
            "description": "\"Pinned by … until …\" — who placed it, the department when it is a\ndepartment's pin, and when it ends in the reader's time zone (a time\ntoday, a weekday within the week, else a date).\n",
            "example": "Pinned by Luis Ortega until Thu"
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "The store a store pin is for."
          },
          "organizational_department_id": {
            "type": "integer",
            "nullable": true,
            "description": "The department a department pin is for."
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Deprecated — the date of `starts_at`, for older clients."
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Deprecated — for older clients."
          }
        }
      },
      "RestBreakConfirmation": {
        "type": "object",
        "description": "A rest-break question waiting for the caller, put off from an earlier shift\nto this clock-in. Answer it at `POST /break_attestations/{id}/answer`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "work_date": {
            "type": "string",
            "format": "date",
            "description": "The day the question is about."
          },
          "work_day_label": {
            "type": "string",
            "example": "Monday, October 6"
          },
          "statement": {
            "type": "string",
            "description": "The statement the person certifies, worded for that day."
          },
          "questions": {
            "type": "array",
            "description": "The questions to ask, in order, each with its answer options.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      },
      "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"
          }
        }
      },
      "SitesPage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "description": "The live title (a translated one when the detail serves a translation)."
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "page_type": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "nullable": true
          },
          "applies_to_everyone": {
            "type": "boolean",
            "description": "False when the page is restricted to part of the company."
          },
          "version_number": {
            "type": "integer",
            "nullable": true,
            "description": "For a reader, the newest PUBLISHED version (null before the first recorded publish); for an editor of the site, the page's version counter, which counts editor snapshots too."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "site": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "display_name": {
                "type": "string",
                "description": "The name in the caller's language; the original when there is no translation."
              },
              "slug": {
                "type": "string"
              },
              "site_type": {
                "type": "string"
              }
            }
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "owner": {
            "type": "object",
            "nullable": true,
            "description": "Who answers for the page, while still an active member.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "last_reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When someone last marked the page Still accurate."
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Written with help from AI (Knowledge Layer E6), resolved for the caller: true only when the company shows that mark to readers, or the caller is the page's author or an editor of its site. An AI edit to a published page's unpublished draft marks the draft, so readers of the live page are told only once it is published."
          },
          "is_published": {
            "type": "boolean"
          },
          "watching": {
            "type": "boolean",
            "description": "Whether the caller watches the page (`POST` / `DELETE /sites/pages/{page_id}/watch`)."
          },
          "bookmarked": {
            "type": "boolean",
            "description": "Whether the caller bookmarked the page (`POST /sites/bookmarks` adds one)."
          },
          "bookmark_id": {
            "type": "integer",
            "nullable": true,
            "description": "The caller's bookmark of the page — the id `DELETE /sites/bookmarks/{id}` removes; null when not bookmarked."
          },
          "comment_count": {
            "type": "integer",
            "nullable": true,
            "description": "Comments on the page, replies included; null while the company has comments switched off."
          },
          "workflow_status": {
            "$ref": "#/components/schemas/SitesPageWorkflowStatus"
          }
        }
      },
      "SitesPageWorkflowStatus": {
        "type": "object",
        "nullable": true,
        "description": "The status badge a page wears for the people who look after it — the site's editors (owners, editors, Sites admins) and the page's assigned approver; null for everyone else, who see no workflow badge. Sent on every page row (`GET /sites/pages`, the `pages` of `GET /sites/sites/{id}`, and the caller's own lists). The web page list draws one badge per fact; the phone draws one, in the order review state, Draft (`status`), Going stale, expiry. The approval itself is the page's: open it with `GET /sites/pages/{id}` (`approval`) and decide it with `POST /sites/approvals/{page id}/approve | reject | request_changes`.",
        "properties": {
          "review_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "pending_review",
              "changes_requested",
              "rejected",
              "approved"
            ],
            "description": "Where the page stands on approval, while that needs a person (`pending_review`, `changes_requested`, `rejected`) or it is `approved` with something still to publish. Null otherwise, and while the company has content approval switched off."
          },
          "review_status_label": {
            "type": "string",
            "nullable": true,
            "description": "The web's words for it — \"Pending approval\", \"Changes requested\", \"Rejected\", \"Approved\", or \"Request withdrawn\" / \"Approval withdrawn\" when an edit withdrew it.",
            "example": "Pending approval"
          },
          "stale": {
            "type": "boolean",
            "description": "Going stale — published and neither republished nor marked Still accurate within the company's review interval."
          },
          "expiry": {
            "type": "object",
            "nullable": true,
            "description": "The page's expiry date, when it has one — the same block as the page detail's `expiry`. \"Expires in N days\" counts to `expires_at`.",
            "properties": {
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "expired": {
                "type": "boolean"
              },
              "expiring_soon": {
                "type": "boolean",
                "description": "Within the next 7 days."
              }
            }
          }
        }
      },
      "SitesPagePerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SitesTrustLine": {
        "type": "object",
        "description": "The page's foot line — \"Owned by <owner_name> · <label> <at>\". The page detail's `trust_line`, and the answer to POST /sites/pages/{id}/mark_accurate.",
        "properties": {
          "owner_name": {
            "type": "string",
            "nullable": true,
            "description": "The page's owner or owning team while still active; null names no one."
          },
          "label": {
            "type": "string",
            "enum": [
              "Published",
              "Checked",
              "Last updated"
            ],
            "description": "`Checked` when someone marked it Still accurate since the last publish; `Last updated` on a draft or an editor's draft body."
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SitesPageDetail": {
        "type": "object",
        "description": "What the page's own screen shows around its body (`GET /sites/pages/{id}` only) — each band answered by the rule the web page asks. A band the caller is not shown is null.",
        "properties": {
          "trust_line": {
            "$ref": "#/components/schemas/SitesTrustLine"
          },
          "audience_label": {
            "type": "string",
            "nullable": true,
            "description": "Who may open a restricted page (\"Restricted to …\"), shown to every reader while the company uses audiences; null when it is open to everyone."
          },
          "audience": {
            "type": "object",
            "nullable": true,
            "description": "The same audience as `audience_label`, as data, under the same rule (null when the page is open to everyone or the company does not use audiences) — for a client that composes its own line, e.g. \"Visible to Operations · New York, Austin and San Francisco\".",
            "properties": {
              "groups": {
                "type": "array",
                "description": "One entry per rule type, in the order department, location, organizational_role, job_family, job_title, group, platform_role, user. Empty when no rule names anyone who still exists — then only admins may open the page.",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "department",
                        "location",
                        "organizational_role",
                        "job_family",
                        "job_title",
                        "group",
                        "platform_role",
                        "user"
                      ]
                    },
                    "names": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            },
            "example": {
              "groups": [
                {
                  "type": "department",
                  "names": [
                    "Operations"
                  ]
                },
                {
                  "type": "location",
                  "names": [
                    "New York",
                    "Austin",
                    "San Francisco"
                  ]
                }
              ]
            }
          },
          "expiry": {
            "type": "object",
            "nullable": true,
            "description": "The page's expiry date, when it has one.",
            "properties": {
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "expired": {
                "type": "boolean"
              },
              "expiring_soon": {
                "type": "boolean",
                "description": "Within the next 7 days — when the web page header badges it."
              }
            }
          },
          "in_short": {
            "type": "object",
            "nullable": true,
            "description": "The \"In short\" summary, when the company and the page show one, it describes the published text and nothing on the page has its own audience. Never over an editor's draft body.",
            "properties": {
              "summary": {
                "type": "string"
              },
              "machine_written": {
                "type": "boolean",
                "description": "False for a translation's summary a person typed — then it is not captioned as written by AI."
              },
              "language": {
                "type": "string",
                "nullable": true,
                "description": "The translation's language code; null for the original."
              }
            }
          },
          "reactions": {
            "type": "object",
            "description": "The page's reaction bar.",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Every reaction on the page."
              },
              "counts": {
                "type": "array",
                "description": "Most-reacted first.",
                "items": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SitesReaction"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              },
              "mine": {
                "type": "array",
                "description": "The reactions the caller left.",
                "items": {
                  "type": "string"
                }
              },
              "available": {
                "type": "array",
                "description": "What the caller may react with — the company's palette, plus any reaction the caller already holds.",
                "items": {
                  "$ref": "#/components/schemas/SitesReaction"
                }
              }
            }
          },
          "version": {
            "type": "object",
            "nullable": true,
            "description": "The live version — the newest PUBLISHED one (never an editor's snapshot); null before the first recorded publish.",
            "properties": {
              "number": {
                "type": "integer"
              },
              "published_at": {
                "type": "string",
                "format": "date-time"
              },
              "published_by": {
                "$ref": "#/components/schemas/SitesPagePerson"
              },
              "change_note": {
                "type": "string",
                "nullable": true,
                "description": "The publisher's \"What changed?\" note."
              },
              "history_enabled": {
                "type": "boolean",
                "description": "Whether the company keeps version history (so \"See what changed\" can be offered)."
              }
            }
          },
          "update_notice": {
            "type": "object",
            "nullable": true,
            "description": "\"Updated since you last read it on <last_read_at>: <note>\" — only when the page was republished WITH a change note since the caller's previous read. Asked before this read is recorded.",
            "properties": {
              "last_read_at": {
                "type": "string",
                "format": "date-time"
              },
              "note": {
                "type": "string"
              },
              "from_version": {
                "type": "integer",
                "nullable": true,
                "description": "The version live at the caller's previous read."
              },
              "to_version": {
                "type": "integer",
                "nullable": true,
                "description": "The version live now."
              },
              "comparable": {
                "type": "boolean",
                "description": "True when \"See what changed\" can compare the two (version history on",
                "two different versions).": null
              }
            }
          },
          "approval": {
            "type": "object",
            "nullable": true,
            "description": "The page's Approval card and panel, for the people the approval is about — its editors, the approver, whoever asked and its author — while a request is in flight or decided, and to an editor on an unpublished page or beside unpublished changes. Null for everyone else, and with page approvals switched off. Names and notes go only to editors, the approver and whoever asked.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "pending_review",
                  "approved",
                  "rejected",
                  "changes_requested"
                ]
              },
              "status_label": {
                "type": "string",
                "nullable": true,
                "example": "Pending approval"
              },
              "summary": {
                "type": "string",
                "nullable": true,
                "description": "The card's one line",
                "e.g. \"Waiting on your decision.\" or \"Waiting for approval from Kevin Robinson.\"": null
              },
              "waiting_on_you": {
                "type": "boolean",
                "description": "The caller decides it."
              },
              "approver": {
                "$ref": "#/components/schemas/SitesPagePerson"
              },
              "requested_by": {
                "$ref": "#/components/schemas/SitesPagePerson"
              },
              "decided_by": {
                "$ref": "#/components/schemas/SitesPagePerson"
              },
              "requested_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "reviewed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "waiting_long": {
                "type": "boolean",
                "description": "Waiting longer than the approvals queue flags."
              },
              "request_note": {
                "type": "string",
                "nullable": true,
                "description": "The note written when approval was asked for."
              },
              "decision_note": {
                "type": "string",
                "nullable": true,
                "description": "The approver's note on the decision."
              },
              "withdrawn_by_edit": {
                "type": "boolean",
                "description": "The author's own edit withdrew the request; nobody decided it."
              },
              "audience_changes": {
                "type": "array",
                "description": "Sections whose audience changes when this is published — what the approver cannot see in the body.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "audience": {
                      "type": "string"
                    }
                  }
                }
              },
              "schedule_text": {
                "type": "string",
                "nullable": true,
                "description": "For the approver of a scheduled draft — what approving does to the schedule."
              },
              "revision": {
                "type": "string",
                "nullable": true,
                "description": "For the approver only — send it as `review_revision` to `POST /sites/approvals/{id}/approve`, `/reject` or `/request_changes`."
              },
              "last_published_version": {
                "type": "integer",
                "nullable": true,
                "description": "For the approver — the live version \"What changed\" compares the draft against."
              },
              "actions": {
                "type": "array",
                "description": "The decisions the caller may take through the Approvals API, in order: `approve`, `request_changes`, `reject` (the approver, while it waits). The web panel's Withdraw, Publish and Request approval have no API door and are never listed.",
                "items": {
                  "type": "string",
                  "enum": [
                    "approve",
                    "request_changes",
                    "reject"
                  ]
                }
              }
            }
          },
          "care": {
            "type": "object",
            "nullable": true,
            "description": "For whoever looks after the page — its owner and anyone who may edit it. Null for everyone else.",
            "properties": {
              "stale": {
                "type": "object",
                "nullable": true,
                "description": "Going stale — published, and neither republished nor marked Still accurate within the company's review interval.",
                "properties": {
                  "since": {
                    "type": "string",
                    "format": "date-time",
                    "description": "The last publish or Still accurate check."
                  },
                  "checked": {
                    "type": "boolean",
                    "description": "True when `since` is a Still accurate check rather than a publish."
                  },
                  "review_interval_days": {
                    "type": "integer"
                  }
                }
              },
              "can_mark_accurate": {
                "type": "boolean",
                "description": "The caller may mark the page Still accurate (its owner or an editor; published pages only) — POST /sites/pages/{id}/mark_accurate."
              },
              "source_refresh": {
                "type": "object",
                "nullable": true,
                "description": "Editors only — a source the page is built from changed.",
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "import_update",
                      "pending",
                      "drift",
                      "in_draft"
                    ],
                    "description": "`import_update` — the web page it was imported from changed and an update waits; `pending` — a refresh is staged for review; `drift` — a bound source changed since it was published; `in_draft` — the refresh is in the draft and only needs publishing."
                  },
                  "prunable": {
                    "type": "boolean",
                    "description": "A staged refresh drops linked records that no longer exist."
                  }
                }
              }
            }
          },
          "workflow": {
            "type": "object",
            "nullable": true,
            "description": "Editors only — the header's own badges.",
            "properties": {
              "unpublished_changes": {
                "type": "boolean"
              },
              "scheduled_publish_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "A draft's scheduled publish."
              }
            }
          },
          "viewer": {
            "type": "object",
            "properties": {
              "can_edit": {
                "type": "boolean"
              },
              "can_bookmark": {
                "type": "boolean",
                "description": "False on a site in the Trash."
              },
              "can_watch": {
                "type": "boolean",
                "description": "False while the company has follows switched off."
              },
              "can_delete": {
                "type": "boolean",
                "description": "Move to Trash (`DELETE /sites/pages/{id}`) — the site's owners and company admins / Sites app-admins, with a `destructive:sites` token; false while the page or its site is still being imported."
              },
              "can_archive": {
                "type": "boolean",
                "description": "Archive (`POST /sites/pages/{id}/archive`) — the site's owners and editors and company admins / Sites app-admins, with a `destructive:sites` token, on a page not already archived; false while the page or its site is still being imported."
              }
            }
          },
          "share_url": {
            "type": "string",
            "nullable": true,
            "description": "The stable page link (survives a rename or a move); published pages off the Trash only."
          },
          "can_share": {
            "type": "boolean",
            "description": "May the caller share this page (POST /shares): anyone who can open it, with a `write:shares` token. On the page detail (GET /sites/pages/{id}) only."
          },
          "web_view_url": {
            "type": "string",
            "example": "https://acme.workforce.mangoapps.com/m/apps/sites/298/pages/259?embed=1",
            "description": "The page's CONTENT alone, for the app's WebView — its sections and nothing else: no site header, title, badges, reactions, actions, comments or Ask AI (draw those from this payload). The body is the one the web page shows the caller: the draft for an editor of the site and for the approver deciding a pending change, else the live body in the caller's language, cut to the sections their audience may read. Carries `locale` when this request asked for one. Open it with the web session the app gets from `POST /auth/webview_token` → `/m/login/native?token=…&return_to=<this URL's path and query>`. Opening it records no read; this request is what counts as one."
          }
        }
      },
      "SitesDirectoryGroupRef": {
        "type": "object",
        "description": "The All sites group a site files under: its For field's first axis (a place level, plural — `place-<depth>`; a department branch — `departments` or `department-<id>`; `formats` — Brands and formats), else its category (`category-<site_type>`), titled in the tenant's words.",
        "properties": {
          "key": {
            "type": "string",
            "example": "category-team"
          },
          "title": {
            "type": "string",
            "example": "Team"
          }
        }
      },
      "SitesDirectoryGroup": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SitesDirectoryGroupRef"
          },
          {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer",
                "description": "The sites in this group across the whole filtered set."
              }
            }
          }
        ]
      },
      "SitesSite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "The name as written (for editing). Draw `display_name`."
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "The description as written (for editing). Draw `display_description`."
          },
          "display_name": {
            "type": "string",
            "description": "The name in the caller's language (Knowledge Layer P6); the original when there is no translation."
          },
          "display_description": {
            "type": "string",
            "nullable": true,
            "description": "The description in the caller's language; the original when there is no translation."
          },
          "site_type": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "icon": {
            "type": "string",
            "nullable": true
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "icon_color": {
            "type": "string",
            "nullable": true,
            "description": "The icon's own colour on site cards and lists (a hex such as `#be123c`); null is Automatic — the site's colour, else its category's tone."
          },
          "cover_tone": {
            "type": "string",
            "nullable": true,
            "enum": [
              "light",
              "dark"
            ],
            "description": "How the cover reads under the site's name — `light` takes dark ink, `dark` light ink, each over a scrim of the opposite tone at `cover_scrim` alpha. Null while the cover is not measured; light ink over a 0.6 black scrim reads on any picture."
          },
          "cover_scrim": {
            "type": "number",
            "nullable": true,
            "description": "The alpha (0–1) of the scrim behind the name and description that clears WCAG AA on the measured cover; null with `cover_tone`."
          },
          "pages_count": {
            "type": "integer",
            "description": "The pages the site's page list shows the caller (`GET /sites/pages?site_id=`, and `pages` on `GET /sites/sites/{id}`): every page to a Sites admin and to the site's owners and editors; to anyone else the pages their audience admits that are published, their own, or sent to them for review — the number the desktop tile prints."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "member_role": {
            "type": "string",
            "nullable": true,
            "description": "The caller's role in the site, if they are a member."
          },
          "is_published": {
            "type": "boolean"
          },
          "is_draft": {
            "type": "boolean",
            "description": "The site is a draft (`status` is `draft`) — only its members and admins can open it. The Draft chip; with `can_publish`, the draft site's \"Publish site\" button under Follow."
          },
          "following": {
            "type": "boolean",
            "description": "Whether the caller follows this site (`POST` / `DELETE /sites/sites/{site_id}/follow`)."
          },
          "parent_site_id": {
            "type": "integer",
            "nullable": true,
            "description": "The site this one sits under (E3); null for a top-level site."
          },
          "allows_child_sites": {
            "type": "boolean",
            "description": "Whether sites may be placed under this one."
          },
          "for_criteria": {
            "type": "object",
            "description": "Who the site is For (the Audience::Dimensions criteria shape) — only the axes that name anyone; `{}` when it is For nobody in particular.",
            "properties": {
              "department_ids": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Departments, and the departments under them."
              },
              "location_ids": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Places, and the places under them."
              },
              "location_attribute_values": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "\"key=value\" place attributes, e.g. \"store_format=hub\"."
              }
            },
            "example": {
              "department_ids": [
                "12"
              ]
            }
          },
          "group": {
            "$ref": "#/components/schemas/SitesDirectoryGroupRef"
          },
          "chip": {
            "type": "string",
            "nullable": true,
            "description": "Why the site is the caller's, for the chip All sites puts on it — `Member`, `Following`, `Your department`, `Your <place level>` (in the tenant's words, e.g. `Your region`), `Your location`, or `For you`; null when it is not theirs (no chip).",
            "example": "Member"
          },
          "sites_inside": {
            "type": "integer",
            "description": "The sites beneath this one at any depth that the caller may open (\"2 sites inside\")."
          },
          "last_updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When a reader last saw the site change — the later of the site row's `updated_at` and its newest page change the caller can read (a page write never touches the site row)."
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the caller may edit the site — a Sites admin, or an owner or editor of it (by membership, or by a Sites grant that manages the places or departments the site is For)."
          },
          "can_archive": {
            "type": "boolean",
            "description": "Archive (`POST /sites/sites/{id}/archive`) would not be refused — the site's owners and editors and company admins / Sites app-admins, with a `destructive:sites` token, on a site not already archived; false while the site is still being imported."
          },
          "can_trash": {
            "type": "boolean",
            "description": "Move to Trash (`POST /sites/sites/{id}/trash`) would not be refused — the site's owners and company admins / Sites app-admins, with a `destructive:sites` token; false while the site is still being imported."
          },
          "can_publish": {
            "type": "boolean",
            "description": "Publish (`POST /sites/sites/{id}/publish`) would not be refused — the site's owners and editors and company admins / Sites app-admins, with a `write:sites` token, on a site that is not already published, not in Trash, not still being imported and whose imported audience is not still waiting for a Sites admin. True on an archived site too (the web's Manage ▾ › Publish); the draft site's \"Publish site\" prompt is `is_draft && can_publish`."
          },
          "restricted": {
            "type": "boolean",
            "description": "True only to those who may edit the site (`can_edit`), while the tenant's audience restrictions are on, when the site is open to a limited audience — the Restricted chip. Always false for a reader."
          }
        }
      },
      "SitesSearchSite": {
        "description": "A site the search found — a site row of the caller's own lists, plus where it sits.",
        "allOf": [
          {
            "description": "A row that is a site (Following, a saved site) — SitesListSite plus what a site row draws.",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesListSite"
              },
              {
                "type": "object",
                "properties": {
                  "display_description": {
                    "type": "string",
                    "nullable": true,
                    "description": "The description in the caller's language; the original when there is no translation."
                  },
                  "restricted": {
                    "type": "boolean",
                    "description": "The site's audience is restricted — shown only to people who may edit the site (and Sites admins), while the tenant's audience restrictions are on; always false for everyone else (the All sites tile's Restricted chip)."
                  }
                }
              }
            ]
          },
          {
            "type": "object",
            "required": [
              "pages_count",
              "sites_inside",
              "parents"
            ],
            "properties": {
              "pages_count": {
                "type": "integer",
                "description": "The pages the site's own list shows the caller (draw \"3 pages\")."
              },
              "sites_inside": {
                "type": "integer",
                "description": "The sites beneath this one, at any depth, that the caller may open (draw \"2 sites inside\"; nothing when 0)."
              },
              "parents": {
                "type": "array",
                "description": "The sites this one sits in, root first (at most two), naming only sites the caller may open; empty for a top-level site or when the caller may not open its parent. Draw it as \"in Operations › Midwest\".",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "display_name": {
                      "type": "string",
                      "description": "The name in the caller's language; the original when there is no translation."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "SitesSearchResult": {
        "type": "object",
        "required": [
          "id",
          "title",
          "slug",
          "page_type",
          "updated_at",
          "site"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "page_type": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "excerpt": {
            "type": "string",
            "nullable": true,
            "description": "The sentence around the first match in the body text the caller may read (or the description), with \"…\" where it was cut; for a page matched by its title alone, its description (up to 200 characters). Null when there is neither."
          },
          "matched_term": {
            "type": "string",
            "nullable": true,
            "description": "The words in `excerpt` that matched, exactly as the page writes them (to highlight); null for a page matched by its title alone."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "site": {
            "description": "The page's site as every list draws it — draw `display_name` under the title, and the site's `icon` on its `tile_color` as the row's icon.",
            "allOf": [
              {
                "description": "A site as the caller's lists draw it — the icon on its tile colour.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitesRef"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "slug": {
                        "type": "string"
                      },
                      "site_type": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string",
                        "description": "`draft` draws the Draft badge (only the site's members and Sites admins can open a draft site)."
                      },
                      "icon": {
                        "type": "string",
                        "nullable": true,
                        "description": "The site's Font Awesome icon class; null draws the default globe."
                      },
                      "icon_color": {
                        "type": "string",
                        "nullable": true,
                        "description": "The site's own Icon colour (hex); null is Automatic."
                      },
                      "tile_color": {
                        "type": "string",
                        "description": "The hex the site's icon tile paints — its Icon colour, else its own or inherited colour, else its category's tone. Never null."
                      }
                    }
                  }
                ]
              }
            ]
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "description": "The page's author; null when the author is not recorded.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "SitesBookmark": {
        "type": "object",
        "required": [
          "id",
          "created_at",
          "bookmarkable_type",
          "page",
          "site"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the caller bookmarked it."
          },
          "bookmarkable_type": {
            "type": "string",
            "enum": [
              "Site",
              "SitePage"
            ]
          },
          "page": {
            "nullable": true,
            "description": "The bookmarked page; null for a bookmark of a site.",
            "allOf": [
              {
                "description": "A page as every one of the caller's lists draws it.",
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "title": {
                    "type": "string"
                  },
                  "display_title": {
                    "type": "string",
                    "description": "The title in the caller's language when a complete translation exists, else the original."
                  },
                  "slug": {
                    "type": "string"
                  },
                  "page_type": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "description": "The page's status. Lists hold only pages the caller can open, so a reader sees `published`; an editor or an assigned reviewer can also see `draft` (draw the Draft badge)."
                  },
                  "published_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "site": {
                    "$ref": "#/components/schemas/SitesListSite"
                  },
                  "workflow_status": {
                    "type": "object",
                    "nullable": true,
                    "description": "The status badge a page wears for the people who look after it — the site's editors (owners, editors, Sites admins) and the page's assigned approver; null for everyone else, who see no workflow badge. Sent on every page row (`GET /sites/pages`, the `pages` of `GET /sites/sites/{id}`, and the caller's own lists). The web page list draws one badge per fact; the phone draws one, in the order review state, Draft (`status`), Going stale, expiry. The approval itself is the page's: open it with `GET /sites/pages/{id}` (`approval`) and decide it with `POST /sites/approvals/{page id}/approve | reject | request_changes`.",
                    "properties": {
                      "review_status": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                          "pending_review",
                          "changes_requested",
                          "rejected",
                          "approved"
                        ],
                        "description": "Where the page stands on approval, while that needs a person (`pending_review`, `changes_requested`, `rejected`) or it is `approved` with something still to publish. Null otherwise, and while the company has content approval switched off."
                      },
                      "review_status_label": {
                        "type": "string",
                        "nullable": true,
                        "description": "The web's words for it — \"Pending approval\", \"Changes requested\", \"Rejected\", \"Approved\", or \"Request withdrawn\" / \"Approval withdrawn\" when an edit withdrew it.",
                        "example": "Pending approval"
                      },
                      "stale": {
                        "type": "boolean",
                        "description": "Going stale — published and neither republished nor marked Still accurate within the company's review interval."
                      },
                      "expiry": {
                        "type": "object",
                        "nullable": true,
                        "description": "The page's expiry date, when it has one — the same block as the page detail's `expiry`. \"Expires in N days\" counts to `expires_at`.",
                        "properties": {
                          "expires_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "expired": {
                            "type": "boolean"
                          },
                          "expiring_soon": {
                            "type": "boolean",
                            "description": "Within the next 7 days."
                          }
                        }
                      }
                    }
                  }
                }
              }
            ]
          },
          "site": {
            "allOf": [
              {
                "description": "A row that is a site (Following, a saved site) — SitesListSite plus what a site row draws.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitesListSite"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "display_description": {
                        "type": "string",
                        "nullable": true,
                        "description": "The description in the caller's language; the original when there is no translation."
                      },
                      "restricted": {
                        "type": "boolean",
                        "description": "The site's audience is restricted — shown only to people who may edit the site (and Sites admins), while the tenant's audience restrictions are on; always false for everyone else (the All sites tile's Restricted chip)."
                      }
                    }
                  }
                ]
              }
            ],
            "description": "The bookmarked site — a site row, with `display_description` and `restricted` — or, for a page bookmark, the page's site (the SitesListSite the page sits on, without those two). Draw `display_name` rather than `name`."
          }
        }
      },
      "SitesApprovalApproveRequest": {
        "type": "object",
        "required": [
          "review_revision"
        ],
        "properties": {
          "review_revision": {
            "type": "string",
            "description": "The row's `review.revision`, as the caller loaded it."
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "description": "Optional — anything the person who asked should know."
          }
        }
      },
      "SitesApprovalNoteRequest": {
        "type": "object",
        "required": [
          "review_revision",
          "notes"
        ],
        "properties": {
          "review_revision": {
            "type": "string",
            "description": "The row's `review.revision`, as the caller loaded it."
          },
          "notes": {
            "type": "string",
            "description": "Required — sent to the person who asked for approval. Leading and trailing spaces are dropped."
          }
        }
      },
      "SitesApprovalDecision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The page's id."
          },
          "decision": {
            "type": "string",
            "enum": [
              "approve",
              "reject",
              "request_changes"
            ]
          },
          "message": {
            "type": "string",
            "description": "The web's confirmation — \"Page approved.\", \"Page rejected.\" or \"Changes requested.\""
          },
          "title": {
            "type": "string",
            "description": "The name that was decided (a drafted rename's)."
          },
          "requested_by": {
            "nullable": true,
            "description": "Who asked for approval — who is told about the decision.",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              }
            ]
          },
          "review": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "approved",
                  "rejected",
                  "changes_requested"
                ]
              },
              "status_label": {
                "type": "string",
                "example": "Changes requested"
              },
              "reviewed_at": {
                "type": "string",
                "format": "date-time"
              },
              "decided_by": {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              },
              "note": {
                "type": "string",
                "nullable": true,
                "description": "The note sent with the decision."
              }
            }
          },
          "waiting_count": {
            "type": "integer",
            "description": "The caller's Waiting on you count after the decision — the Approvals badge (`GET /apps` → `sites_approvals`)."
          },
          "waiting_label": {
            "type": "string",
            "description": "What that count is made of, for a screen reader."
          }
        }
      },
      "SitesApprovalPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SitesApprovalRow": {
        "type": "object",
        "description": "One page sent for approval, as the web Approvals row draws it.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The page's id."
          },
          "kind": {
            "type": "string",
            "enum": [
              "request",
              "sent"
            ],
            "description": "`request` — someone's request (decide it, or see who has it); `sent` — the caller's own request, as Sent by me draws it (on Waiting on you both kinds appear)."
          },
          "title": {
            "type": "string",
            "description": "The name being approved — a published page's drafted rename, which goes live with the approved content."
          },
          "renames": {
            "type": "string",
            "nullable": true,
            "description": "The live title a drafted rename replaces (\"renames “…”\"); null without one."
          },
          "page": {
            "nullable": true,
            "description": "The page and its site as every Sites list draws them; null only when the page's site is gone (the row offers no actions).",
            "allOf": [
              {
                "description": "A page as every one of the caller's lists draws it.",
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "title": {
                    "type": "string"
                  },
                  "display_title": {
                    "type": "string",
                    "description": "The title in the caller's language when a complete translation exists, else the original."
                  },
                  "slug": {
                    "type": "string"
                  },
                  "page_type": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "description": "The page's status. Lists hold only pages the caller can open, so a reader sees `published`; an editor or an assigned reviewer can also see `draft` (draw the Draft badge)."
                  },
                  "published_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "site": {
                    "$ref": "#/components/schemas/SitesListSite"
                  },
                  "workflow_status": {
                    "type": "object",
                    "nullable": true,
                    "description": "The status badge a page wears for the people who look after it — the site's editors (owners, editors, Sites admins) and the page's assigned approver; null for everyone else, who see no workflow badge. Sent on every page row (`GET /sites/pages`, the `pages` of `GET /sites/sites/{id}`, and the caller's own lists). The web page list draws one badge per fact; the phone draws one, in the order review state, Draft (`status`), Going stale, expiry. The approval itself is the page's: open it with `GET /sites/pages/{id}` (`approval`) and decide it with `POST /sites/approvals/{page id}/approve | reject | request_changes`.",
                    "properties": {
                      "review_status": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                          "pending_review",
                          "changes_requested",
                          "rejected",
                          "approved"
                        ],
                        "description": "Where the page stands on approval, while that needs a person (`pending_review`, `changes_requested`, `rejected`) or it is `approved` with something still to publish. Null otherwise, and while the company has content approval switched off."
                      },
                      "review_status_label": {
                        "type": "string",
                        "nullable": true,
                        "description": "The web's words for it — \"Pending approval\", \"Changes requested\", \"Rejected\", \"Approved\", or \"Request withdrawn\" / \"Approval withdrawn\" when an edit withdrew it.",
                        "example": "Pending approval"
                      },
                      "stale": {
                        "type": "boolean",
                        "description": "Going stale — published and neither republished nor marked Still accurate within the company's review interval."
                      },
                      "expiry": {
                        "type": "object",
                        "nullable": true,
                        "description": "The page's expiry date, when it has one — the same block as the page detail's `expiry`. \"Expires in N days\" counts to `expires_at`.",
                        "properties": {
                          "expires_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "expired": {
                            "type": "boolean"
                          },
                          "expiring_soon": {
                            "type": "boolean",
                            "description": "Within the next 7 days."
                          }
                        }
                      }
                    }
                  }
                }
              }
            ]
          },
          "person": {
            "nullable": true,
            "description": "Whose face leads the row — the requester on a request, the approver (who has it, or who decided it) on the caller's own. Null when no approver was chosen.",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              },
              {
                "type": "object",
                "properties": {
                  "avatar_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Absolute URL; null draws initials."
                  }
                }
              }
            ]
          },
          "requested_by": {
            "nullable": true,
            "description": "Who asked for approval (on a request; null on the caller's own).",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              }
            ]
          },
          "approver": {
            "nullable": true,
            "description": "Who the request was sent to; null when no approver was chosen.",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              }
            ]
          },
          "decided_by": {
            "nullable": true,
            "description": "Who approved it or sent it back (on the caller's own, once decided).",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesApprovalPerson"
              }
            ]
          },
          "review": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending_review",
                  "approved",
                  "changes_requested",
                  "rejected"
                ]
              },
              "status_label": {
                "type": "string",
                "nullable": true,
                "description": "The state's words on a decided row of the caller's own — \"Changes requested\", \"Rejected\", \"Request withdrawn\", \"Approved — ready to publish\", \"Approved — scheduled for …\", \"Approved — waiting for … to publish\". Null while waiting."
              },
              "badge": {
                "type": "string",
                "enum": [
                  "decided",
                  "waiting_long",
                  "asked"
                ],
                "description": "`decided` — draw `status_label` as a status badge; `waiting_long` — waiting past `wait_flag_after_days`, draw a Waiting badge from `requested_at`; `asked` — the wait as quiet text (\"asked 2 hours ago\")."
              },
              "wait_flag_after_days": {
                "type": "integer",
                "example": 3
              },
              "requested_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When approval was asked for."
              },
              "reviewed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When it was decided."
              },
              "withdrawn_by_edit": {
                "type": "boolean",
                "description": "The author's own edit voided the request; no approver decided it (\"the page was edited after you sent it\")."
              },
              "approved_state": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "ready",
                  "scheduled",
                  "waiting"
                ],
                "description": "An approved request not live yet: `ready` — the caller may publish it now; `scheduled` — its publish time will publish it; `waiting` — someone else publishes it (`status_label` says who)."
              },
              "note": {
                "type": "string",
                "nullable": true,
                "description": "The request note on a request; the approver's note on a decided row of the caller's own. Only for the approver, the requester and the site's editors — null for anyone else."
              },
              "scheduled_publish_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "schedule_text": {
                "type": "string",
                "nullable": true,
                "description": "What the schedule means for this row, e.g. \"This page is scheduled for Oct 21, 9:00 AM — approving lets it go live then.\""
              },
              "revision": {
                "type": "string",
                "nullable": true,
                "description": "The page's review revision, which a decision sends back so it is refused if the page changed; null when the caller cannot decide."
              }
            }
          },
          "waiting_for": {
            "type": "string",
            "nullable": true,
            "description": "When the caller can neither decide nor act on it, who the decision belongs to — \"Waiting for Dana Whitfield\" or \"No approver chosen\"."
          },
          "actions": {
            "type": "array",
            "description": "The decisions the caller may take on this row through this API, in the order the web draws them: `request_changes`, `reject`, `approve` (POST /sites/approvals/{id}/request_changes, /reject, /approve) — only to whoever may decide it. The web row's other actions (What changed, Publish, Withdraw, Edit page, Open page) have no API door and are never listed. Empty when there is nothing to decide.",
            "items": {
              "type": "string",
              "enum": [
                "request_changes",
                "reject",
                "approve"
              ]
            }
          },
          "last_published_version": {
            "type": "integer",
            "nullable": true,
            "description": "The last published version, which the web's What changed diffs against the draft; null on a first approval."
          }
        }
      },
      "SitesCommentReactionWrite": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "page_id": {
                "type": "integer"
              },
              "emoji": {
                "type": "string",
                "description": "The glyph acted on (a catalog key is answered as its glyph)."
              },
              "reacted": {
                "type": "boolean",
                "description": "Whether the caller now holds this reaction."
              },
              "reactions": {
                "$ref": "#/components/schemas/SitesReactionSummary"
              }
            }
          },
          {
            "type": "object",
            "properties": {
              "comment_id": {
                "type": "integer",
                "description": "The comment or reply acted on."
              },
              "reactions": {
                "allOf": [
                  {
                    "type": "object",
                    "description": "The page's reaction bar.",
                    "properties": {
                      "total": {
                        "type": "integer",
                        "description": "Every reaction on the page."
                      },
                      "counts": {
                        "type": "array",
                        "description": "Most-reacted first.",
                        "items": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/SitesReaction"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "count": {
                                  "type": "integer"
                                }
                              }
                            }
                          ]
                        }
                      },
                      "mine": {
                        "type": "array",
                        "description": "The reactions the caller left.",
                        "items": {
                          "type": "string"
                        }
                      },
                      "available": {
                        "type": "array",
                        "description": "What the caller may react with — the company's palette, plus any reaction the caller already holds.",
                        "items": {
                          "$ref": "#/components/schemas/SitesReaction"
                        }
                      }
                    }
                  }
                ],
                "description": "The comment's reaction bar (not the page's)."
              }
            }
          }
        ]
      },
      "SitesDashboardDecisionCard": {
        "type": "object",
        "description": "One decision waiting on the caller, with its count and the first item.",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "sites.reviews",
              "sites.sent_back",
              "sites.ready_to_publish",
              "sites.scheduled"
            ],
            "description": "`sites.reviews` Pending approval (requests the caller decides) · `sites.sent_back` Sent back to you · `sites.ready_to_publish` Ready to publish · `sites.scheduled` Scheduled to publish."
          },
          "title": {
            "type": "string",
            "example": "Pending approval"
          },
          "count": {
            "type": "integer"
          },
          "count_label": {
            "type": "string",
            "nullable": true,
            "description": "Drawn instead of `count` when present — \"2 overdue\" on the scheduled card."
          },
          "tone": {
            "type": "string",
            "enum": [
              "info",
              "amber",
              "success",
              "danger"
            ]
          },
          "icon": {
            "type": "string",
            "example": "fas fa-clipboard-check"
          },
          "lead": {
            "type": "string",
            "nullable": true,
            "description": "The first page's title."
          },
          "lead_page_id": {
            "type": "integer",
            "nullable": true,
            "description": "The first page — open it with `GET /sites/pages/{id}`."
          },
          "lead_site_id": {
            "type": "integer",
            "nullable": true
          },
          "sub": {
            "type": "string",
            "nullable": true,
            "example": "People & Culture · waiting 6 days"
          },
          "cta": {
            "type": "object",
            "description": "The list that holds exactly the counted set.",
            "properties": {
              "label": {
                "type": "string",
                "example": "Open approvals"
              },
              "list": {
                "type": "string",
                "enum": [
                  "approvals",
                  "queue"
                ]
              },
              "filter": {
                "type": "string",
                "nullable": true,
                "description": "With `list: approvals` — the Approvals filter (`mine` Assigned to me, `sent` Sent by me, `ready` Ready to publish); null is All pending (a Sites admin's approvals card)."
              },
              "queue": {
                "type": "string",
                "description": "With `list: queue` — the page queue (`scheduled`)."
              },
              "web_path": {
                "type": "string",
                "example": "/apps/sites/reviews?filter=mine"
              }
            }
          }
        }
      },
      "SitesDashboardCareRow": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "owner",
              "sources",
              "links",
              "stale",
              "expiring",
              "reading"
            ]
          },
          "title": {
            "type": "string",
            "example": "Going stale"
          },
          "count": {
            "type": "integer",
            "description": "Pages with this problem."
          },
          "count_text": {
            "type": "string",
            "example": "2 pages",
            "description": "\"3 links on 2 pages\" for broken links."
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "cta_label": {
            "type": "string",
            "example": "Review pages"
          },
          "queue": {
            "type": "string",
            "description": "The page queue the row opens (`/apps/sites/queue?queue=<queue>` on the web)."
          }
        }
      },
      "SitesDashboardSiteRow": {
        "type": "object",
        "properties": {
          "site": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "display_name": {
                "type": "string",
                "description": "The name in the caller's language; the original when there is no translation."
              },
              "slug": {
                "type": "string"
              },
              "site_type": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "The web marks a site that is not published with its status."
              },
              "is_published": {
                "type": "boolean"
              },
              "icon": {
                "type": "string",
                "nullable": true
              },
              "icon_color": {
                "type": "string",
                "nullable": true
              },
              "cover_image": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "pages_count": {
            "type": "integer",
            "description": "Pages on the site",
            "every status. 0 is \"No pages yet\".": null
          },
          "pages_needing_care": {
            "type": "integer",
            "description": "\"N of M pages\" need care."
          },
          "reads": {
            "type": "integer",
            "nullable": true,
            "description": "Page opens over `reads_days`. Null while analytics is off."
          },
          "reads_previous": {
            "type": "integer",
            "nullable": true
          },
          "trend_percent": {
            "type": "integer",
            "nullable": true
          },
          "last_published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null is \"Not yet\"."
          }
        }
      },
      "SitesDashboardPage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "site": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "display_name": {
                "type": "string"
              }
            }
          }
        }
      },
      "SitesRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "description": "The name in the caller's language; the original when there is no translation."
          }
        }
      },
      "SitesListSite": {
        "description": "A site as the caller's lists draw it — the icon on its tile colour.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SitesRef"
          },
          {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "site_type": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "`draft` draws the Draft badge (only the site's members and Sites admins can open a draft site)."
              },
              "icon": {
                "type": "string",
                "nullable": true,
                "description": "The site's Font Awesome icon class; null draws the default globe."
              },
              "icon_color": {
                "type": "string",
                "nullable": true,
                "description": "The site's own Icon colour (hex); null is Automatic."
              },
              "tile_color": {
                "type": "string",
                "description": "The hex the site's icon tile paints — its Icon colour, else its own or inherited colour, else its category's tone. Never null."
              }
            }
          }
        ]
      },
      "SitesListSiteRow": {
        "description": "A row that is a site (Following, a saved site) — SitesListSite plus what a site row draws.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SitesListSite"
          },
          {
            "type": "object",
            "properties": {
              "display_description": {
                "type": "string",
                "nullable": true,
                "description": "The description in the caller's language; the original when there is no translation."
              },
              "restricted": {
                "type": "boolean",
                "description": "The site's audience is restricted — shown only to people who may edit the site (and Sites admins), while the tenant's audience restrictions are on; always false for everyone else (the All sites tile's Restricted chip)."
              }
            }
          }
        ]
      },
      "SitesPageSummary": {
        "description": "A page as every one of the caller's lists draws it.",
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "display_title": {
            "type": "string",
            "description": "The title in the caller's language when a complete translation exists, else the original."
          },
          "slug": {
            "type": "string"
          },
          "page_type": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "The page's status. Lists hold only pages the caller can open, so a reader sees `published`; an editor or an assigned reviewer can also see `draft` (draw the Draft badge)."
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "site": {
            "$ref": "#/components/schemas/SitesListSite"
          },
          "workflow_status": {
            "type": "object",
            "nullable": true,
            "description": "The status badge a page wears for the people who look after it — the site's editors (owners, editors, Sites admins) and the page's assigned approver; null for everyone else, who see no workflow badge. Sent on every page row (`GET /sites/pages`, the `pages` of `GET /sites/sites/{id}`, and the caller's own lists). The web page list draws one badge per fact; the phone draws one, in the order review state, Draft (`status`), Going stale, expiry. The approval itself is the page's: open it with `GET /sites/pages/{id}` (`approval`) and decide it with `POST /sites/approvals/{page id}/approve | reject | request_changes`.",
            "properties": {
              "review_status": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "pending_review",
                  "changes_requested",
                  "rejected",
                  "approved"
                ],
                "description": "Where the page stands on approval, while that needs a person (`pending_review`, `changes_requested`, `rejected`) or it is `approved` with something still to publish. Null otherwise, and while the company has content approval switched off."
              },
              "review_status_label": {
                "type": "string",
                "nullable": true,
                "description": "The web's words for it — \"Pending approval\", \"Changes requested\", \"Rejected\", \"Approved\", or \"Request withdrawn\" / \"Approval withdrawn\" when an edit withdrew it.",
                "example": "Pending approval"
              },
              "stale": {
                "type": "boolean",
                "description": "Going stale — published and neither republished nor marked Still accurate within the company's review interval."
              },
              "expiry": {
                "type": "object",
                "nullable": true,
                "description": "The page's expiry date, when it has one — the same block as the page detail's `expiry`. \"Expires in N days\" counts to `expires_at`.",
                "properties": {
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "expired": {
                    "type": "boolean"
                  },
                  "expiring_soon": {
                    "type": "boolean",
                    "description": "Within the next 7 days."
                  }
                }
              }
            }
          }
        }
      },
      "SitesRecentPage": {
        "description": "A page on Recent — SitesPageSummary plus what the Recent row draws.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SitesPageSummary"
          },
          {
            "type": "object",
            "properties": {
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "description": "When a reader last saw the page change — its last publish when live, else its last edit."
              },
              "site": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitesListSite"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "parent": {
                        "nullable": true,
                        "description": "The site this one sits in, named only when the caller may open it; null for a top-level site or a parent the caller cannot open.",
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/SitesRef"
                          }
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SitesComment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "body": {
            "type": "string",
            "description": "Plain text. Each `@[Name](mention:<id>)` mention reads \"@Name\", the name resolved from the id."
          },
          "body_html": {
            "type": "string",
            "description": "The body escaped as HTML, line breaks as `<br>`, each resolved mention a `span.mention-tag[data-user-id]`."
          },
          "parent_id": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "author": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "reaction_counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "👍": 2
            }
          },
          "my_reactions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "can_edit": {
            "type": "boolean",
            "description": "The caller may edit this comment (PATCH) — they wrote it."
          },
          "can_delete": {
            "type": "boolean",
            "description": "The caller may delete this comment (DELETE) — they wrote it, or they are a company admin / Sites app-admin or an owner or editor of the page's site."
          },
          "replies": {
            "type": "array",
            "description": "Top-level comments only (GET); oldest first.",
            "items": {
              "$ref": "#/components/schemas/SitesComment"
            }
          }
        }
      },
      "SitesPagination": {
        "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"
          }
        }
      },
      "SitesPersonRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SitesMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The membership's id."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          },
          "role_label": {
            "type": "string",
            "example": "Editor"
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "avatar_url": {
                "type": "string",
                "description": "Absolute; an initials tile when the person has no photo."
              }
            }
          },
          "is_current_user": {
            "type": "boolean"
          },
          "can_remove": {
            "type": "boolean",
            "description": "The caller may remove this member — their own row too, to leave the site (never the site's last owner)."
          },
          "added_by": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesPersonRef"
              }
            ],
            "nullable": true,
            "description": "Who granted the membership; only for those who manage the site's members."
          }
        }
      },
      "SitesMemberSection": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "owners",
              "editors",
              "readers"
            ]
          },
          "title": {
            "type": "string",
            "example": "Owners"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          },
          "count": {
            "type": "integer",
            "description": "Members of this role on the whole site."
          },
          "items": {
            "type": "array",
            "description": "This page's members of this role.",
            "items": {
              "$ref": "#/components/schemas/SitesMember"
            }
          },
          "audience": {
            "type": "object",
            "description": "`readers` only — who can read the site without a membership.",
            "properties": {
              "restricted": {
                "type": "boolean"
              },
              "title": {
                "type": "string"
              },
              "text": {
                "type": "string"
              },
              "rules": {
                "type": "array",
                "nullable": true,
                "items": {
                  "type": "string"
                },
                "description": "A restricted site's audience rules; only for those who manage the site's members."
              }
            }
          }
        }
      },
      "SitesMemberRoster": {
        "type": "object",
        "properties": {
          "site": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "display_name": {
                "type": "string",
                "description": "The name in the caller's language."
              }
            }
          },
          "can_remove": {
            "type": "boolean",
            "description": "The caller may remove members of this site."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitesMemberSection"
            }
          },
          "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"
              }
            }
          }
        }
      },
      "SitesMemberRemoved": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation",
            "in the caller's language.": null
          },
          "removed": {
            "type": "object",
            "properties": {
              "user": {
                "$ref": "#/components/schemas/SitesPersonRef"
              },
              "role": {
                "type": "string",
                "enum": [
                  "owner",
                  "editor",
                  "viewer"
                ]
              }
            }
          },
          "left_site": {
            "type": "boolean",
            "description": "The caller removed themselves."
          },
          "pages_handed_on": {
            "type": "object",
            "nullable": true,
            "description": "Null when the person owned no pages on the site.",
            "properties": {
              "count": {
                "type": "integer"
              },
              "new_owner": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitesPersonRef"
                  }
                ],
                "nullable": true,
                "description": "Null when no active owner or editor was left to take them."
              }
            }
          }
        }
      },
      "SitesCareQueue": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "care",
              "owner",
              "sources",
              "links",
              "stale",
              "expiring",
              "reading"
            ]
          },
          "title": {
            "type": "string"
          },
          "cta_label": {
            "type": "string",
            "description": "The words on the list's fix (absent on `care`)."
          },
          "count": {
            "type": "integer",
            "description": "On GET /sites/care_queues only — a list page's count is its `total_count`."
          },
          "detail": {
            "type": "string",
            "description": "The Dashboard row's one specific line",
            "when the list has pages.": null
          }
        }
      },
      "SitesCarePageRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "site_id": {
            "type": "integer"
          }
        }
      },
      "SitesCareRow": {
        "type": "object",
        "properties": {
          "page": {
            "description": "A page as every one of the caller's lists draws it.",
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "display_title": {
                "type": "string",
                "description": "The title in the caller's language when a complete translation exists, else the original."
              },
              "slug": {
                "type": "string"
              },
              "page_type": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "The page's status. Lists hold only pages the caller can open, so a reader sees `published`; an editor or an assigned reviewer can also see `draft` (draw the Draft badge)."
              },
              "published_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "site": {
                "$ref": "#/components/schemas/SitesListSite"
              },
              "workflow_status": {
                "type": "object",
                "nullable": true,
                "description": "The status badge a page wears for the people who look after it — the site's editors (owners, editors, Sites admins) and the page's assigned approver; null for everyone else, who see no workflow badge. Sent on every page row (`GET /sites/pages`, the `pages` of `GET /sites/sites/{id}`, and the caller's own lists). The web page list draws one badge per fact; the phone draws one, in the order review state, Draft (`status`), Going stale, expiry. The approval itself is the page's: open it with `GET /sites/pages/{id}` (`approval`) and decide it with `POST /sites/approvals/{page id}/approve | reject | request_changes`.",
                "properties": {
                  "review_status": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "pending_review",
                      "changes_requested",
                      "rejected",
                      "approved"
                    ],
                    "description": "Where the page stands on approval, while that needs a person (`pending_review`, `changes_requested`, `rejected`) or it is `approved` with something still to publish. Null otherwise, and while the company has content approval switched off."
                  },
                  "review_status_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "The web's words for it — \"Pending approval\", \"Changes requested\", \"Rejected\", \"Approved\", or \"Request withdrawn\" / \"Approval withdrawn\" when an edit withdrew it.",
                    "example": "Pending approval"
                  },
                  "stale": {
                    "type": "boolean",
                    "description": "Going stale — published and neither republished nor marked Still accurate within the company's review interval."
                  },
                  "expiry": {
                    "type": "object",
                    "nullable": true,
                    "description": "The page's expiry date, when it has one — the same block as the page detail's `expiry`. \"Expires in N days\" counts to `expires_at`.",
                    "properties": {
                      "expires_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "expired": {
                        "type": "boolean"
                      },
                      "expiring_soon": {
                        "type": "boolean",
                        "description": "Within the next 7 days."
                      }
                    }
                  }
                }
              }
            }
          },
          "site": {
            "description": "A site as the caller's lists draw it — the icon on its tile colour.",
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesRef"
              },
              {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "site_type": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "description": "`draft` draws the Draft badge (only the site's members and Sites admins can open a draft site)."
                  },
                  "icon": {
                    "type": "string",
                    "nullable": true,
                    "description": "The site's Font Awesome icon class; null draws the default globe."
                  },
                  "icon_color": {
                    "type": "string",
                    "nullable": true,
                    "description": "The site's own Icon colour (hex); null is Automatic."
                  },
                  "tile_color": {
                    "type": "string",
                    "description": "The hex the site's icon tile paints — its Icon colour, else its own or inherited colour, else its category's tone. Never null."
                  }
                }
              }
            ]
          },
          "kind": {
            "type": "string",
            "description": "The list's problem; on `care`, the page's first problem."
          },
          "kinds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On `care`, every problem the page has (the web's Needs column); else the list's key."
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "description": "Why the page is here",
            "in one short line.": null
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The last \"Still accurate\" check."
          },
          "freshness_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The later of the last publish and the last check — what Going stale measures."
          },
          "confirm_by": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "A must-read page's confirm-by date."
          },
          "owner": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "active": {
                "type": "boolean",
                "description": "False when the owner has left — the page waits on nobody."
              }
            }
          },
          "actions": {
            "type": "object",
            "description": "The fixes the caller may take on this row.",
            "properties": {
              "mark_accurate": {
                "type": "boolean",
                "description": "POST /sites/pages/{id}/mark_accurate"
              },
              "extend_expiry": {
                "type": "boolean",
                "description": "POST /sites/pages/{page_id}/expiry_extension"
              },
              "remind": {
                "type": "boolean",
                "description": "POST /sites/pages/{page_id}/read_confirmation_reminders"
              },
              "review_refresh": {
                "type": "boolean",
                "description": "GET /sites/pages/{page_id}/source_refresh"
              }
            }
          }
        }
      },
      "SitesSourceRefresh": {
        "type": "object",
        "properties": {
          "page": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesCarePageRef"
              },
              {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "source",
              "import_update"
            ]
          },
          "staged": {
            "type": "boolean",
            "description": "A proposal is saved and ready to apply."
          },
          "has_content_change": {
            "type": "boolean"
          },
          "changes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What changed in the source page (import_update)."
          },
          "drift": {
            "type": "array",
            "description": "The bound sources that changed (source).",
            "items": {
              "type": "object",
              "properties": {
                "section_id": {
                  "type": "string"
                },
                "section_type": {
                  "type": "string"
                },
                "section_name": {
                  "type": "string"
                },
                "heading": {
                  "type": "string",
                  "nullable": true
                },
                "issue": {
                  "type": "string",
                  "enum": [
                    "removed",
                    "source_updated"
                  ]
                },
                "source_label": {
                  "type": "string"
                },
                "missing_records": {
                  "type": "integer",
                  "description": "Linked records no longer available",
                  "removed from the section.": null
                }
              }
            }
          },
          "proposed_sections": {
            "type": "array",
            "description": "The page body after the refresh, section by section.",
            "items": {
              "type": "object",
              "properties": {
                "position": {
                  "type": "integer"
                },
                "type": {
                  "type": "string"
                },
                "type_name": {
                  "type": "string",
                  "nullable": true
                },
                "heading": {
                  "type": "string",
                  "nullable": true
                },
                "excerpt": {
                  "type": "string",
                  "nullable": true
                },
                "differs_from_draft": {
                  "type": "boolean"
                }
              }
            }
          },
          "proposal_digest": {
            "type": "string",
            "description": "A fingerprint of `proposed_sections` — send it back with apply."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "can_apply": {
            "type": "boolean"
          },
          "can_publish": {
            "type": "boolean"
          },
          "publish_refusal": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SitesRemovedPage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "site_id": {
            "type": "integer"
          }
        }
      },
      "SitesPageTrashed": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "page": {
            "$ref": "#/components/schemas/SitesRemovedPage"
          },
          "trash": {
            "type": "object",
            "properties": {
              "restorable_until": {
                "type": "string",
                "format": "date-time",
                "description": "Until when the site's owners and editors can restore it (as a draft) from the site's Trash."
              },
              "restore_window_days": {
                "type": "integer",
                "description": "How long a page stays restorable in Trash."
              },
              "asked_to_confirm": {
                "type": "boolean",
                "description": "The page had versions that asked readers to confirm they had read it; those come back with it on restore, with the record of who confirmed."
              }
            }
          }
        }
      },
      "SitesPageArchived": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "page": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesRemovedPage"
              },
              {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "archived"
                    ]
                  }
                }
              }
            ]
          }
        }
      },
      "SitesReaction": {
        "type": "object",
        "description": "One reaction as the API names it.",
        "properties": {
          "emoji": {
            "type": "string",
            "example": "👍",
            "description": "The stored glyph."
          },
          "key": {
            "type": "string",
            "nullable": true,
            "example": "like",
            "description": "The reaction's catalog key; null for a glyph outside the catalog."
          },
          "label": {
            "type": "string",
            "example": "Like",
            "description": "The reaction's name — say it beside the icon."
          },
          "icon_url": {
            "type": "string",
            "nullable": true,
            "description": "The drawn icon the web shows (absolute); null when the reaction is drawn as its glyph."
          }
        }
      },
      "SitesReactionSummary": {
        "type": "object",
        "description": "The page's reaction bar.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Every reaction on the page."
          },
          "counts": {
            "type": "array",
            "description": "Most-reacted first.",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/SitesReaction"
                },
                {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              ]
            }
          },
          "mine": {
            "type": "array",
            "description": "The reactions the caller left.",
            "items": {
              "type": "string"
            }
          },
          "available": {
            "type": "array",
            "description": "What the caller may react with — the company's palette, plus any reaction the caller already holds.",
            "items": {
              "$ref": "#/components/schemas/SitesReaction"
            }
          }
        }
      },
      "SitesReactionRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SitesReaction"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "reacted_at": {
                "type": "string",
                "format": "date-time"
              },
              "user": {
                "type": "object",
                "description": "Who reacted. `name` is \"Unknown\" for someone who has since left the organization.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "initials": {
                    "type": "string",
                    "nullable": true
                  },
                  "job_title": {
                    "type": "string",
                    "nullable": true
                  },
                  "avatar_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Absolute."
                  }
                }
              }
            }
          }
        ]
      },
      "SitesReactionWrite": {
        "type": "object",
        "properties": {
          "page_id": {
            "type": "integer"
          },
          "emoji": {
            "type": "string",
            "description": "The glyph acted on (a catalog key is answered as its glyph)."
          },
          "reacted": {
            "type": "boolean",
            "description": "Whether the caller now holds this reaction."
          },
          "reactions": {
            "$ref": "#/components/schemas/SitesReactionSummary"
          }
        }
      },
      "SitesReadConfirmation": {
        "type": "object",
        "description": "Whether the live version of a page asks THIS caller to confirm reading it. `{ requested: false }` when it does not.",
        "properties": {
          "requested": {
            "type": "boolean"
          },
          "acknowledged": {
            "type": "boolean"
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "version_number": {
            "type": "integer"
          },
          "due_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "overdue": {
            "type": "boolean"
          }
        }
      },
      "SitesSitePublished": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "site": {
            "allOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "description": "The name as written."
                  }
                }
              },
              {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "published"
                    ]
                  },
                  "is_draft": {
                    "type": "boolean",
                    "description": "Always false once published."
                  },
                  "published_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            ]
          }
        }
      },
      "SitesRemovedSite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "The name as written."
          }
        }
      },
      "SitesSiteArchived": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "site": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesRemovedSite"
              },
              {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "archived"
                    ]
                  },
                  "archived_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            ]
          }
        }
      },
      "SitesSiteTrashed": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "site": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitesRemovedSite"
              },
              {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "The status the site had — Trash is not a status, and a restored site comes back as a draft."
                  },
                  "trashed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it was moved to Trash."
                  }
                }
              }
            ]
          }
        }
      },
      "SitesStillAccuratePerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SitesStillAccurate": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The web's confirmation, in the caller's language."
          },
          "page": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "site_id": {
                "type": "integer"
              }
            }
          },
          "still_accurate": {
            "type": "object",
            "properties": {
              "checked_at": {
                "type": "string",
                "format": "date-time",
                "description": "The check — the page's new freshness date."
              },
              "checked_by": {
                "$ref": "#/components/schemas/SitesStillAccuratePerson"
              },
              "review_interval_days": {
                "type": "integer",
                "description": "The company's review interval — how long the page stays off Going stale."
              },
              "next_check_due_at": {
                "type": "string",
                "format": "date-time",
                "description": "When it goes stale again without another check or a republish."
              },
              "became_owner": {
                "type": "boolean",
                "description": "The page had no owner and the caller is now its owner."
              },
              "owner": {
                "nullable": true,
                "description": "The page's owner after the check, while still an active member; null when it has none.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitesStillAccuratePerson"
                  }
                ]
              }
            }
          },
          "trust_line": {
            "type": "object",
            "description": "The page's foot line — \"Owned by <owner_name> · <label> <at>\". The page detail's `trust_line`, and the answer to POST /sites/pages/{id}/mark_accurate.",
            "properties": {
              "owner_name": {
                "type": "string",
                "nullable": true,
                "description": "The page's owner or owning team while still active; null names no one."
              },
              "label": {
                "type": "string",
                "enum": [
                  "Published",
                  "Checked",
                  "Last updated"
                ],
                "description": "`Checked` when someone marked it Still accurate since the last publish; `Last updated` on a draft or an editor's draft body."
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "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
              }
            }
          }
        }
      },
      "SopHubSopSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Open set; today draft, published, archived, retired."
          },
          "sop_type": {
            "type": "string",
            "description": "Open set."
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "difficulty_level": {
            "type": "string",
            "nullable": true,
            "description": "Open set."
          },
          "step_count": {
            "type": "integer"
          },
          "estimated_minutes": {
            "type": "integer",
            "nullable": true
          },
          "version_number": {
            "type": "integer"
          },
          "requires_acknowledgement": {
            "type": "boolean"
          },
          "requires_verification": {
            "type": "boolean"
          },
          "completion_validity_days": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "SopHubPerson": {
        "type": "object",
        "nullable": true,
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SopHubCompletion": {
        "type": "object",
        "properties": {
          "completion_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "SopCompletion#status. Open set; today pending, in_progress, completed, pending_verification, re_completion_required, overdue."
          },
          "status_label": {
            "type": "string",
            "description": "The worker's word for the status — the web's own label, and \"Sent back\" for a completion a SOP Hub admin sent back. On the HR review list (and the verify / send-back answers) it is the web review list's label instead."
          },
          "completed_steps": {
            "type": "integer"
          },
          "total_steps": {
            "type": "integer"
          },
          "completion_percentage": {
            "type": "number"
          },
          "completion_method": {
            "type": "string",
            "nullable": true,
            "description": "Open set; server-set, never from the request."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "acknowledged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "version_completed": {
            "type": "integer",
            "nullable": true
          },
          "valid_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "re_completion_deadline": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "days_until_deadline": {
            "type": "integer",
            "nullable": true
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "verified_by": {
            "$ref": "#/components/schemas/SopHubPerson"
          },
          "verification_notes": {
            "type": "string",
            "nullable": true,
            "description": "The signer's note, when the completion is signed off and they left one."
          },
          "re_completion_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why a finished completion is open again — sent back, the procedure's steps changed, or it expired — the sentence the web, /m/ and QR pages print. Null when it is not re-opened, and always null on the HR review list (and the verify / send-back answers): it is written to the worker."
          },
          "sign_off_waiting_on": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SopHubPerson"
            },
            "description": "Whom a completion awaiting sign-off waits on (the people its sign-off request was sent to; empty when none is on record). Only on a single completion — GET /sop_hub/sops/{id}, its start, step and submit, and GET /sop_hub/qr/{code} — never on a list's rows."
          },
          "sent_back": {
            "type": "object",
            "nullable": true,
            "properties": {
              "by": {
                "$ref": "#/components/schemas/SopHubPerson"
              },
              "reason": {
                "type": "string"
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "steps_reset_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set while the completion is open and its progress was cleared because the SOP's steps changed — the web's \"This procedure was updated …, so your progress was reset\" line. Null otherwise."
          },
          "sop": {
            "$ref": "#/components/schemas/SopHubSopSummary"
          },
          "version_changed": {
            "$ref": "#/components/schemas/SopHubVersionChanged"
          },
          "can_complete": {
            "type": "boolean",
            "description": "On the caller's own lists (dashboard, my_pending, my_history): something is still owed, the SOP is published and its required training is done."
          },
          "step_completions": {
            "type": "array",
            "description": "Present on a SOP's own completion and on every write.",
            "items": {
              "type": "object",
              "properties": {
                "step_position": {
                  "type": "integer"
                },
                "completed_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "notes": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "SopHubVersionChanged": {
        "type": "object",
        "nullable": true,
        "description": "On the caller's own lists and a SOP's own completion: set while the SOP's version is past the one the caller completed — the web's \"Updated: version N to M · K steps changed\" line. Null otherwise.",
        "properties": {
          "from": {
            "type": "integer",
            "description": "The version the caller completed."
          },
          "to": {
            "type": "integer",
            "description": "The SOP's version now."
          },
          "steps_changed": {
            "type": "integer",
            "nullable": true,
            "description": "Steps added, changed, moved or removed since; null when the earlier version's steps are not on record."
          }
        }
      },
      "SopHubLibraryRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SopHubSopSummary"
          },
          {
            "type": "object",
            "properties": {
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "published_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          }
        ]
      },
      "SopHubFilterOption": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The value to send back as the filter."
          },
          "label": {
            "type": "string",
            "description": "The web's words, in the caller's language."
          },
          "count": {
            "type": "integer",
            "description": "SOPs this option would list, with the caller's other filters applied."
          }
        }
      },
      "SopHubStatusCounts": {
        "type": "object",
        "description": "The status chips. Counted with every filter applied except the status, so a chip's number is what choosing it lists.",
        "properties": {
          "total": {
            "type": "integer"
          },
          "published": {
            "type": "integer"
          },
          "draft": {
            "type": "integer"
          },
          "archived": {
            "type": "integer"
          },
          "retired": {
            "type": "integer"
          }
        }
      },
      "SopHubManual": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "sop_count": {
            "type": "integer"
          }
        }
      },
      "SopHubPageMeta": {
        "type": "object",
        "properties": {
          "total_count": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "current_page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "next_page": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "SopHubReviewCompletion": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SopHubCompletion"
          },
          {
            "type": "object",
            "properties": {
              "user": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SopHubPerson"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "nullable": true,
                        "description": "The person's email, as the web list prints it under their name — only where the People directory shows the caller email; null otherwise."
                      }
                    }
                  }
                ]
              },
              "member": {
                "type": "boolean",
                "description": "The worker is still a member of the business."
              },
              "is_sample_data": {
                "type": "boolean"
              },
              "can_verify": {
                "type": "boolean",
                "description": "Awaiting sign-off, and not the caller's own completion."
              },
              "can_send_back": {
                "type": "boolean",
                "description": "Awaiting sign-off on a published SOP, not the caller's own, worker still a member."
              },
              "last_send_back": {
                "type": "object",
                "nullable": true,
                "description": "Set when this submission answers a send-back: who sent it back, why, and when. null for a first submission.",
                "properties": {
                  "by": {
                    "$ref": "#/components/schemas/SopHubPerson"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        ]
      },
      "SopHubVersion": {
        "type": "object",
        "properties": {
          "version_number": {
            "type": "integer"
          },
          "label": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "change_summary": {
            "type": "string",
            "nullable": true,
            "description": "The change line in the caller's language. Nobody types it: SOP Hub writes it when it snapshots a version (\"Initial version\", \"Changed: steps and title\", \"Restored from v2 (changed: steps)\"), and a version with none falls back to its change_reason's words. `change` says the same as data."
          },
          "change": {
            "type": "object",
            "description": "What the change line says, for a client that words it itself.",
            "properties": {
              "kind": {
                "type": "string",
                "description": "Open set; today initial · snapshot (recorded when a completion was filed against a version that had none) · fields_changed · restored · reason (no line stored: word `reason`) · text (other stored words, shown as `text`)."
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "For fields_changed and restored: the fields changed (steps, title, description, type, duration, roles, locations — an open set)."
              },
              "restored_from": {
                "type": "integer",
                "nullable": true,
                "description": "For restored: the version it was restored from."
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "The version's change_reason (open set)."
              },
              "text": {
                "type": "string",
                "nullable": true,
                "description": "For text: the stored words."
              }
            }
          },
          "change_reason": {
            "type": "string",
            "nullable": true,
            "description": "Open set."
          },
          "was_published": {
            "type": "boolean"
          },
          "step_count": {
            "type": "integer"
          },
          "created_by": {
            "$ref": "#/components/schemas/SopHubPerson"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SopHubStep": {
        "type": "object",
        "nullable": true,
        "properties": {
          "title": {
            "type": "string",
            "nullable": true
          },
          "content": {
            "type": "string",
            "nullable": true
          },
          "estimated_minutes": {
            "type": "integer",
            "nullable": true
          },
          "verification_required": {
            "type": "boolean"
          },
          "media_url": {
            "type": "string",
            "nullable": true
          },
          "safety": {
            "$ref": "#/components/schemas/SopHubStepSafety"
          }
        }
      },
      "SopHubStepSafety": {
        "type": "object",
        "nullable": true,
        "description": "The step's safety facts, drawn before its instructions: hazards, required PPE\nand tools, and the expected outcome (\"Done when\"). Null when the step carries\nnone. On a translated reading they are the translated step's when it carries\ntranslated ones, else the original step's at the same position, in English;\n`language` says which.\n",
        "properties": {
          "hazards": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "severity": {
                  "type": "string",
                  "enum": [
                    "info",
                    "caution",
                    "warning",
                    "danger"
                  ]
                },
                "label": {
                  "type": "string",
                  "description": "The severity's word in the client's language (Note, Caution, Warning, Danger)."
                },
                "description": {
                  "type": "string"
                }
              }
            }
          },
          "ppe": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expected_outcome": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true,
            "description": "The language code the facts read in on a translated reading (\"en\" when they were not translated); null on an untranslated one."
          }
        }
      },
      "SopHubReminderDispatch": {
        "type": "object",
        "properties": {
          "queued": {
            "type": "integer",
            "description": "Reminders queued; they are sent in the background."
          },
          "throttled": {
            "type": "integer",
            "description": "Held back: reminded within the tenant's frequency, or at the reminder limit."
          },
          "tracked": {
            "type": "boolean",
            "description": "A Background Task tracks the delivery."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "SopHubError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Open set."
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "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. On a conversation survey, its fixed questions only."
          },
          "response_mode": {
            "type": "string",
            "enum": [
              "form",
              "conversation"
            ],
            "description": "How people answer: the form, or a conversation with the AI interviewer. A conversation survey is taken on its take page (`conversation.take_url`) and refuses `respond`."
          },
          "conversation": {
            "type": "object",
            "nullable": true,
            "description": "Present only on a conversation survey.",
            "properties": {
              "minutes": {
                "type": "integer",
                "description": "How long the conversation takes, as its guide states it."
              },
              "take_url": {
                "type": "string",
                "format": "uri",
                "description": "The web take page, which starts or resumes the caller's conversation."
              },
              "available": {
                "type": "boolean",
                "description": "False while new conversations can't start in the organization (conversation mode off, or its AI usage blocked), or — on a survey that names its respondents — once the caller's own monthly AI allowance is spent."
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "description": "Why the conversation isn't available, in the words the Take list uses; null when it is."
              }
            }
          },
          "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",
          "team_view_available",
          "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 of their own team."
          },
          "team_view_available": {
            "type": "boolean",
            "description": "`true` when the caller reads this survey's whole-survey aggregate AND may read their team's slice of it too, with `scope=team` — offer both views. `false` where there is only one view: full results, an employee's aggregate, or a manager's team slice on a survey that does not share its results with employees."
          },
          "team_reach": {
            "type": "object",
            "description": "PRESENT ONLY when `scope` is `team`: whose answers the slice holds. `requested` is the `reach` asked for (`direct` when omitted or not one of the two values); `shown` is the slice actually rolled up. They differ, with `narrowed_to_direct: true`, when `reach=all` fell back to the direct reports because too few people below them answered — the web and phone pages say \"Showing results from your direct reports only\" in that case, and a client should too. They also differ, with `narrowed_to_direct: false` and `shown: all`, for a caller with no direct reports left who still leads people below them: the direct slice would be nobody, so the slice is everyone below — say \"everyone below you\", as the pages do.",
            "required": [
              "requested",
              "shown",
              "narrowed_to_direct"
            ],
            "properties": {
              "requested": {
                "type": "string",
                "enum": [
                  "direct",
                  "all"
                ]
              },
              "shown": {
                "type": "string",
                "enum": [
                  "direct",
                  "all"
                ]
              },
              "narrowed_to_direct": {
                "type": "boolean"
              }
            }
          },
          "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. `false` in the suppressed branch, except for a team slice withheld beside the whole survey (`anonymity_protected: true`), whose own count did meet it."
          },
          "anonymity_protected": {
            "type": "boolean",
            "description": "PRESENT ONLY when suppression fired — an anonymous survey whose response count for this scope is below the threshold, or a team slice that fewer people than the threshold answered outside of, on an anonymous survey that shares its results with employees. 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 answerable question, in builder order. `rating`, `number`, `slider`, `scale` are rolled up as `numeric`; `select`, `radio`, `checkbox`, `multiselect` as `choice`; `text`, `textarea`, `rich_text` as `text`; every other answerable type (date, time, email, phone, url, file, signature …) as `answered`, a count only. Fields nobody answers (calculated, hidden, lookup …) are omitted. Empty in the suppressed branch.",
            "items": {
              "$ref": "#/components/schemas/SurveyQuestionSummary"
            }
          },
          "shared_summary": {
            "type": "object",
            "description": "The \"You said, we did\" summary the survey shared with its audience — the organization's own words, so no anonymity floor applies. Present for a full-results reader, or for a caller it was sent to; omitted otherwise, and when the survey shared none.",
            "required": [
              "text",
              "shared_on"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "The summary as it was sent."
              },
              "shared_on": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "The day it was shared, in the caller's time zone."
              }
            }
          }
        }
      },
      "SurveySharedSummary": {
        "type": "object",
        "description": "A \"You said, we did\" summary shared with the caller — the organization's own words, so no anonymity floor applies.",
        "required": [
          "survey_id",
          "survey",
          "shared_on",
          "text"
        ],
        "properties": {
          "survey_id": {
            "type": "integer",
            "description": "The survey that shared it."
          },
          "survey": {
            "type": "string",
            "description": "The survey's title.",
            "example": "Rota pulse"
          },
          "shared_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The day it was shared, in the caller's time zone.",
            "example": "2026-09-15"
          },
          "text": {
            "type": "string",
            "description": "The summary as it was sent, plain text.",
            "example": "You told us the rota came late. It now comes two weeks ahead."
          }
        }
      },
      "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 team (direct reports, or everyone below them with `reach=all`) for `team`. `null` on an anonymous survey below the anonymity floor when the caller reads the `team` slice or an invitee's shared summary (the `:aggregate` tier), and for a `team` slice that fewer people than the floor answered outside of on an anonymous survey that shares its results with employees — the web masks the same count."
          },
          "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; `null` when `response_count` is masked."
          }
        }
      },
      "SurveyQuestionSummary": {
        "type": "object",
        "description": "One question's roll-up. `type` selects which of the remaining fields are present, and any of the four 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",
              "answered"
            ]
          },
          "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` and `answered`, the number of non-blank answers."
          },
          "exportable": {
            "type": "boolean",
            "description": "`answered` only. `true` for date, datetime, time, email, phone, url, barcode, QR code and colour questions, whose answers the CSV export shows as written."
          },
          "insufficient_data": {
            "type": "boolean",
            "description": "Present only when `count` is below `minimum_response_threshold`: on `numeric` and `choice` summaries always, on `text` and `answered` summaries only for an anonymous survey. When present, `average`/`min`/`max` and `distribution` are all absent. A `text` summary never carries the answers, at any volume.",
            "example": true
          },
          "average": {
            "type": "number",
            "format": "float",
            "description": "`numeric` only, rounded to 2 decimal places."
          },
          "min": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "`numeric` only. On an ANONYMOUS survey `null` when the lowest answer's score is counted under \"Other answers\" (see `distribution`), and always `null` when the scale is drawn in ranged bars (keys like `\"90–99\"`)."
          },
          "max": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "`numeric` only. On an ANONYMOUS survey `null` when the highest answer's score is counted under \"Other answers\" (see `distribution`), and always `null` when the scale is drawn in ranged bars (keys like `\"90–99\"`)."
          },
          "distribution": {
            "type": "object",
            "description": "For `numeric`, the answers bucketed by rounded value over the question's scale (keys are the scale points, values are counts; a scale wider than eleven points is drawn in labelled bars of equal width, keyed like `\"90–99\"`). For `choice`, 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. On an ANONYMOUS survey an answer is a group of people: a score or option fewer people than `minimum_response_threshold` chose is folded into one `\"Other answers\"` key, counted as the distinct people who chose any folded answer (the same rule a conversation survey's findings apply), and an empty score is not listed — the scores missing beside it would be the folded few.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "integer",
                  "description": "`numeric` — the number of answers at this scale point."
                },
                {
                  "type": "object",
                  "description": "`choice` — this option's count and share.",
                  "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"
          }
        }
      },
      "TrainingLessonHelpTurn": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "conversation_id": {
            "type": "string",
            "description": "The private thread this turn streams on."
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ]
          },
          "websocket": {
            "type": "object",
            "properties": {
              "channel": {
                "type": "string",
                "enum": [
                  "AiResponseChannel"
                ]
              },
              "subscription": {
                "type": "object",
                "properties": {
                  "conversation_id": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "kind": {
            "type": "string",
            "enum": [
              "explain",
              "example",
              "recap",
              "review"
            ]
          },
          "sources": {
            "type": "array",
            "description": "The lessons the answer may cite, in citation order.",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "description": "The citation's label in the reader's language (\"Lesson source\", \"Lesson source 2\")."
                },
                "course_id": {
                  "type": "integer"
                },
                "lesson_id": {
                  "type": "integer"
                },
                "lesson_title": {
                  "type": "string"
                },
                "digest": {
                  "type": "string",
                  "description": "Pass to GET …/help/source to check the citation is still current."
                },
                "anchor": {
                  "type": "string",
                  "enum": [
                    "native-lesson-source"
                  ]
                }
              }
            }
          }
        }
      },
      "TrainingQuizMissedReview": {
        "type": "object",
        "nullable": true,
        "description": "What you missed — the lessons behind the questions a failed attempt got wrong (the web's \"What you missed\"). Null when the attempt passed, or no missed question links a lesson in the learner's course version.",
        "properties": {
          "lessons": {
            "type": "array",
            "description": "Up to five, in course order; each opens the lesson.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 412
                },
                "title": {
                  "type": "string",
                  "example": "Cross-contact"
                },
                "missed": {
                  "type": "integer",
                  "description": "How many missed questions this lesson covers.",
                  "example": 2
                }
              }
            }
          },
          "required": {
            "type": "boolean",
            "description": "The quiz asks for this review before a retake (\"I've reviewed these lessons\", POST /training/quiz_attempts/{id}/review_complete).",
            "example": true
          },
          "completed": {
            "type": "boolean",
            "description": "The learner already said they reviewed it.",
            "example": false
          },
          "recap_available": {
            "type": "boolean",
            "description": "Offer \"Recap these lessons\" (POST …/quiz_attempts/{attempt_id}/review_recap). True exactly when the web shows that button to this person: Ask AI is open to them, the Training assistant is on for the organization and for them, and a linked lesson they may open has text or an approved transcript. When false the recap call is refused.",
            "example": true
          }
        }
      },
      "TrainingReturnReminderResponse": {
        "type": "object",
        "properties": {
          "reminders": {
            "type": "object",
            "properties": {
              "delivery_hour": {
                "type": "integer",
                "nullable": true,
                "enum": [
                  9,
                  12,
                  17,
                  null
                ]
              },
              "enabled": {
                "type": "boolean",
                "description": "Whether the personal training_return notification category is enabled. Delivery also requires a non-null delivery_hour."
              },
              "timezone": {
                "type": "string",
                "description": "Learner's effective timezone name."
              },
              "daily_limit": {
                "type": "integer",
                "enum": [
                  1
                ]
              },
              "weekly_limit": {
                "type": "integer",
                "enum": [
                  2
                ]
              },
              "preference_id": {
                "type": "integer",
                "nullable": true
              },
              "enrollment_id": {
                "type": "integer",
                "nullable": true
              },
              "snoozed_until": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "can_snooze": {
                "type": "boolean",
                "description": "True only for an eligible started course attempt and a caller with write:training."
              }
            }
          },
          "can_change": {
            "type": "boolean",
            "description": "Whether this caller has write:training."
          },
          "unread_notification_count": {
            "type": "integer"
          }
        }
      },
      "TrainingPracticePersona": {
        "type": "object",
        "nullable": true,
        "description": "The persona the learner talks to — on the card and the attempt, the one who opens the conversation. Practice personas carry no image, so `avatar_url` is always null today; draw initials.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 214
          },
          "name": {
            "type": "string",
            "example": "Dana"
          },
          "role": {
            "type": "string",
            "nullable": true,
            "example": "Forklift operator"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "example": null
          }
        }
      },
      "TrainingPracticeCard": {
        "type": "object",
        "properties": {
          "lesson_id": {
            "type": "integer",
            "example": 1349
          },
          "title": {
            "type": "string",
            "example": "Rehearse the pre-shift toolbox talk"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "example": "Lead a 3-minute safety talk after a forklift near-miss."
          },
          "available": {
            "type": "boolean",
            "example": true
          },
          "unavailable_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "no_scenario",
              "unpublished",
              "app_disabled",
              "no_persona",
              null
            ],
            "example": null
          },
          "scenario": {
            "type": "object",
            "nullable": true,
            "description": "Null only when the lesson's scenario no longer exists (`no_scenario`).",
            "properties": {
              "id": {
                "type": "integer",
                "example": 88
              },
              "title": {
                "type": "string",
                "example": "Rehearse the pre-shift toolbox talk"
              },
              "difficulty": {
                "type": "string",
                "enum": [
                  "beginner",
                  "intermediate",
                  "advanced"
                ],
                "example": "beginner"
              },
              "difficulty_label": {
                "type": "string",
                "example": "Beginner"
              },
              "pass_threshold": {
                "type": "integer",
                "description": "Score out of 100 needed to pass.",
                "example": 70
              },
              "estimated_minutes": {
                "type": "integer",
                "nullable": true,
                "example": 6
              },
              "persona": {
                "$ref": "#/components/schemas/TrainingPracticePersona"
              }
            }
          },
          "enrolled": {
            "type": "boolean",
            "example": true
          },
          "completed": {
            "type": "boolean",
            "description": "The lesson is complete on the current enrollment.",
            "example": false
          },
          "best_score": {
            "type": "integer",
            "nullable": true,
            "description": "Best final score (a manager override wins) of this lesson's scored attempts.",
            "example": null
          },
          "attempts_count": {
            "type": "integer",
            "description": "This lesson's attempts on the current enrollment",
            "discarded ones excluded.": null,
            "example": 0
          },
          "open_attempt_id": {
            "type": "integer",
            "nullable": true,
            "description": "An in-progress attempt that starting will resume.",
            "example": null
          },
          "last_attempt": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "example": 5012
              },
              "status": {
                "type": "string",
                "enum": [
                  "in_progress",
                  "completed",
                  "scored"
                ],
                "example": "scored"
              },
              "score": {
                "type": "integer",
                "nullable": true,
                "example": 82
              },
              "passed": {
                "type": "boolean",
                "nullable": true,
                "description": "Null until scored.",
                "example": true
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "can_start": {
            "type": "boolean",
            "description": "Enrolled and available — draw Start (or Resume",
            "with open_attempt_id) only when true.": null,
            "example": true
          }
        }
      },
      "TrainingPracticeTurn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "role": {
            "type": "string",
            "enum": [
              "persona",
              "learner"
            ],
            "example": "persona"
          },
          "content": {
            "type": "string",
            "example": "Morning. Is this about yesterday?"
          },
          "speaker": {
            "type": "string",
            "nullable": true,
            "description": "The persona's name on a persona turn (a panel scenario has several); null on a learner turn.",
            "example": "Dana"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TrainingPracticeScorecard": {
        "type": "object",
        "nullable": true,
        "description": "Present once `status` is `scored`.",
        "properties": {
          "score": {
            "type": "integer",
            "description": "Final score out of 100: a manager's override when there is one, else the AI's.",
            "example": 82
          },
          "ai_score": {
            "type": "integer",
            "nullable": true,
            "example": 82
          },
          "passed": {
            "type": "boolean",
            "description": "The stored outcome — never re-derive it from today's pass threshold.",
            "example": true
          },
          "narrative": {
            "type": "string",
            "nullable": true,
            "example": "You opened with the near-miss and kept it short."
          },
          "strengths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Clear opening."
            ]
          },
          "improvements": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Ask the crew a question."
            ]
          },
          "criteria": {
            "type": "array",
            "description": "Every rubric criterion in authored order.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 31
                },
                "name": {
                  "type": "string",
                  "example": "Names the hazard"
                },
                "score": {
                  "type": "integer",
                  "nullable": true,
                  "description": "0–100, or null when the judge did not score it (a real 0 means the behaviour was absent).",
                  "example": 80
                },
                "max": {
                  "type": "integer",
                  "example": 100
                },
                "comment": {
                  "type": "string",
                  "nullable": true,
                  "example": "Named the blind corner."
                }
              }
            }
          },
          "reviewed": {
            "type": "boolean",
            "description": "A manager has reviewed the score.",
            "example": false
          },
          "review_note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "TrainingPracticeAttempt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 5012
          },
          "lesson_id": {
            "type": "integer",
            "example": 1349
          },
          "course_id": {
            "type": "integer",
            "example": 773
          },
          "status": {
            "type": "string",
            "enum": [
              "in_progress",
              "completed",
              "scored",
              "abandoned"
            ],
            "example": "in_progress"
          },
          "processing_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "queued",
              "scoring",
              "failed",
              null
            ],
            "description": "Poll while `queued` or `scoring`; `failed` → re-POST complete to retry.",
            "example": null
          },
          "scoring_stalled": {
            "type": "boolean",
            "description": "Scoring claims to be queued/scoring but has not moved for 20 minutes — stop polling; POST complete re-queues it.",
            "example": false
          },
          "processing_error": {
            "type": "string",
            "nullable": true,
            "description": "When `processing_status` is `failed`, one fixed sentence written for the learner (the one the web pages show) — never the pipeline's own error text. Null otherwise.",
            "example": null
          },
          "ai_notice": {
            "type": "string",
            "description": "One fixed sentence, in the reader's language, saying the partner is an AI. Always present; a client draws it on the conversation.",
            "example": "You're practicing with an AI, not a real person."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "pass_threshold": {
            "type": "integer",
            "example": 70
          },
          "persona": {
            "$ref": "#/components/schemas/TrainingPracticePersona"
          },
          "turns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrainingPracticeTurn"
            }
          },
          "turns_total": {
            "type": "integer",
            "example": 2
          },
          "turns_truncated": {
            "type": "boolean",
            "description": "True when more than the 120 turns returned exist.",
            "example": false
          },
          "can_reply": {
            "type": "boolean",
            "description": "The run is in progress and Practice Hub is on for the business.",
            "example": true
          },
          "can_complete": {
            "type": "boolean",
            "description": "Practice Hub is on, the learner has replied, and the run is in progress, ended unscored (a retry), or its scoring stalled.",
            "example": false
          },
          "can_discard": {
            "type": "boolean",
            "example": true
          },
          "scorecard": {
            "$ref": "#/components/schemas/TrainingPracticeScorecard"
          },
          "lesson_completed": {
            "type": "boolean",
            "example": false
          },
          "course_status": {
            "type": "string",
            "nullable": true,
            "example": "in_progress"
          },
          "course_progress_percentage": {
            "type": "integer",
            "nullable": true,
            "example": 50
          },
          "unread_notification_count": {
            "$ref": "#/components/schemas/UnreadNotificationCount"
          },
          "_meta": {
            "$ref": "#/components/schemas/ResponseMeta"
          }
        }
      },
      "ManagedCourse": {
        "type": "object",
        "description": "A course published from outside the app, as Training holds it.",
        "required": [
          "key",
          "course",
          "versions",
          "lessons"
        ],
        "properties": {
          "key": {
            "type": "string",
            "example": "app-training:sites"
          },
          "course": {
            "type": "object",
            "required": [
              "id",
              "title",
              "published",
              "url"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "title": {
                "type": "string"
              },
              "published": {
                "type": "boolean",
                "description": "Whether learners can see the course."
              },
              "url": {
                "type": "string",
                "description": "The course's page in Training",
                "absolute.": null
              },
              "cover_checksum": {
                "type": "string",
                "nullable": true,
                "description": "The base64 MD5 of the cover this source sent (`PUT …/cover`), or null when the course shows another image or none."
              }
            }
          },
          "versions": {
            "type": "object",
            "required": [
              "published",
              "draft"
            ],
            "properties": {
              "published": {
                "type": "integer",
                "nullable": true,
                "description": "The number of the live version; null before the first publish."
              },
              "draft": {
                "type": "integer",
                "nullable": true,
                "description": "The number of the draft a publish is working in; null when none is open."
              }
            }
          },
          "build": {
            "type": "object",
            "description": "The build this server runs, so a source can wait until what it filmed is live before it publishes (the fleet's training courses).",
            "properties": {
              "commit": {
                "type": "string",
                "nullable": true,
                "description": "The full commit the running build was made from; null where the server does not know it (a development server)."
              }
            }
          },
          "lessons": {
            "type": "array",
            "description": "The source's own lessons in the draft when one is open, otherwise in the live version — in course order. Lessons someone added by hand in Training are not listed.",
            "items": {
              "$ref": "#/components/schemas/ManagedCourseLesson"
            }
          }
        }
      },
      "ManagedCourseLesson": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "title",
          "quiz_questions"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "video",
              "document"
            ]
          },
          "title": {
            "type": "string"
          },
          "video_checksum": {
            "type": "string",
            "nullable": true,
            "description": "MD5 of the stored video",
            "base64.": null
          },
          "captions_checksum": {
            "type": "string",
            "nullable": true,
            "description": "MD5 of the stored captions (as WebVTT)",
            "base64.": null
          },
          "document_checksum": {
            "type": "string",
            "nullable": true,
            "description": "MD5 of the stored document",
            "base64.": null
          },
          "video_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "The film's length in seconds",
            "as its source sent it; null for a document.": null
          },
          "transcript_md5": {
            "type": "string",
            "nullable": true,
            "description": "MD5 of the film's transcript",
            "hex; null when it has none.": null
          },
          "quiz_questions": {
            "type": "integer",
            "description": "How many questions the quiz after this lesson has; 0 when it has none."
          }
        }
      },
      "ManagedCourseFields": {
        "type": "object",
        "description": "The course's own details. Only the fields sent are applied; a field sent as null is cleared.",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "A Training category by name; created when the business has none by that name."
          },
          "difficulty_level": {
            "type": "string",
            "nullable": true
          },
          "learning_objectives": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ManagedCourseLessonInput": {
        "type": "object",
        "description": "The `lesson` field of `PUT /training/managed_courses/{key}/lessons/{lesson_key}`, sent as a JSON string.",
        "required": [
          "kind",
          "title"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "video",
              "document"
            ]
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "required": {
            "type": "boolean",
            "description": "Whether the lesson counts towards finishing the course. Left out, a video counts and a document does not."
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "True when AI wrote or narrated this lesson. Left out",
            "the lesson keeps its mark.": null
          },
          "duration_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "description": "The video's length. Required for a video lesson."
          },
          "transcript": {
            "type": "string",
            "nullable": true,
            "description": "The video's transcript, kept as sent. Left out with a new video, Training transcribes it."
          },
          "chapters": {
            "type": "array",
            "description": "Where each part of the video starts. The player lists them and moves to the one pressed. In order, and none after the video's end.",
            "items": {
              "type": "object",
              "required": [
                "start",
                "title"
              ],
              "properties": {
                "start": {
                  "type": "number",
                  "description": "Seconds from the start of the video."
                },
                "title": {
                  "type": "string"
                }
              }
            }
          },
          "estimated_minutes": {
            "type": "integer",
            "description": "Reading time of a document lesson."
          },
          "quiz": {
            "type": "object",
            "nullable": true,
            "description": "The quiz that follows the lesson. Left out or null, the lesson has no quiz.",
            "required": [
              "questions"
            ],
            "properties": {
              "title": {
                "type": "string"
              },
              "passing_score": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Percent needed to pass. Left out: the score the quiz already has, or 70."
              },
              "questions": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "required": [
                    "text",
                    "choices",
                    "correct",
                    "explanation"
                  ],
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "choices": {
                      "type": "array",
                      "minItems": 2,
                      "maxItems": 6,
                      "items": {
                        "type": "string"
                      }
                    },
                    "correct": {
                      "type": "integer",
                      "description": "The position of the right choice",
                      "counting from 0.": null
                    },
                    "explanation": {
                      "type": "string",
                      "description": "Why that choice is right — shown to the learner after they answer."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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",
              "fulfilled"
            ],
            "description": "**This is the field that routes a client.** Show a course-completion screen for `completed`; `fulfilled` means the original assignment was satisfied through an approved language course and carries no copied quiz result. 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`: a written answer is not scored, and no staff screen shows it. Say so (\"not scored\") rather than implying an instant score or promising an instructor review."
          },
          "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": {
          "answers_revision": {
            "type": "integer"
          },
          "operation_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "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.** The key keeps its name for compatibility; it means \"not scored\". A `text` answer is never graded (no grading step exists) and is excluded from both sides of the score, so render it as \"Not scored\" — showing it as incorrect tells the learner they failed a question nobody grades."
          },
          "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 not scored and are 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 grades and contradict the review list below it.",
            "properties": {
              "score_percentage": {
                "type": "integer",
                "nullable": true,
                "description": "Null when the quiz has nothing auto-gradable (written answers only): the attempt completes passed with no score. Render \"Not scored\".",
                "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 — not scored, and no staff screen shows them. The name is kept for compatibility; the score above is final and these rows never join it.",
                "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",
              "practice",
              "checklist"
            ],
            "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; a `practice` lesson (scored by Practice Hub) or a `checklist` lesson (signed off by an evaluator); 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"
              },
              "question_ids": {
                "type": "array",
                "nullable": true,
                "items": {
                  "type": "integer"
                },
                "description": "Present only for a quiz that draws a set number of questions per attempt (a question pool): the ids of the questions drawn for this learner, which are the only ones in `questions`. Send it back as `question_ids` when replaying the attempt so it is scored on the questions the device showed. Null for every other quiz."
              },
              "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"
          }
        }
      },
      "TrainingLessonFeedback": {
        "type": "object",
        "properties": {
          "learner_name": {
            "type": "string"
          },
          "course_version_id": {
            "type": "integer"
          },
          "course_version_number": {
            "type": "integer"
          },
          "source_digest": {
            "type": "string"
          },
          "video_position_seconds": {
            "type": "integer",
            "nullable": true
          },
          "can_close": {
            "type": "boolean"
          },
          "id": {
            "type": "integer"
          },
          "training_lesson_id": {
            "type": "integer"
          },
          "reaction": {
            "type": "string",
            "enum": [
              "useful",
              "unclear",
              "outdated"
            ]
          },
          "reason": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true,
            "maxLength": 500
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "in_review",
              "answered",
              "updated",
              "closed"
            ]
          },
          "response": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000
          },
          "responded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "assignee_id": {
            "type": "integer",
            "nullable": true
          },
          "corrected_lesson_id": {
            "type": "integer",
            "nullable": true
          },
          "can_respond": {
            "type": "boolean"
          },
          "can_edit_note": {
            "type": "boolean"
          },
          "can_undo": {
            "type": "boolean"
          }
        }
      },
      "TrainingLessonProgressInput": {
        "type": "object",
        "description": "Additive compatible progress contract. Legacy fields remain optional. Exact resume_position requires operation_id and base_revision. Any request using operation_id, base_revision or resume_position must include enrollment_id from the content checkpoint; a replacement enrollment is rejected with 409. Send progress_generation from the same checkpoint to detect resets. Omission is accepted only for generation zero when using the protected protocol. Legacy unversioned writes may omit enrollment_id. Retries of the identical operation never count time twice. A stale exact revision returns 409; reload rather than overwriting another device's cursor.",
        "properties": {
          "enrollment_id": {
            "type": "integer",
            "description": "Enrollment from the content checkpoint; required for revision/operation saves."
          },
          "progress_generation": {
            "type": "integer",
            "minimum": 0,
            "description": "Reset generation from the content checkpoint; required after the first reset."
          },
          "resume_position": {
            "type": "integer",
            "minimum": 0,
            "maximum": 43200,
            "description": "Exact cursor",
            "including backward seeking.": null
          },
          "base_revision": {
            "type": "integer",
            "minimum": 0,
            "description": "Revision returned by content/progress GET or last successful write."
          },
          "operation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Keep unchanged until the identical operation is acknowledged."
          },
          "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": {
          "enrollment_id": {
            "type": "integer"
          },
          "progress_generation": {
            "type": "integer",
            "minimum": 0
          },
          "course_version_id": {
            "type": "integer"
          },
          "resume_position": {
            "type": "integer",
            "nullable": true
          },
          "revision": {
            "type": "integer"
          },
          "operation_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "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",
              "fulfilled"
            ],
            "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**Print `formatted_date` and `formatted_time`.** They are composed server-side in the VENUE's zone and are ready to render — an instructor-led session happens in a specific place, so that zone is the one that matters, never the viewer's. Formatting `starts_at` yourself (a bare `new Date(starts_at)`) renders in the device's zone and shows a learner in another country a time the session does not start at.\n\nEvery instant here carries the VENUE's UTC offset. `timezone` is for calendar entries and zone-aware arithmetic, not for display; `timezone_label` duplicates the abbreviation that already closes `formatted_time` and is retained only for compatibility.\n\n`starts_at_local` / `ends_at_local` were emitted until 2026-09-01 and are gone. This is not a reversal of that: the venue-local instant is carried by `starts_at` itself rather than duplicated beside it, so there is still ONE representation of the instant.\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."
          },
          "roster_status": {
            "type": "string",
            "enum": [
              "registered",
              "waitlisted",
              "attended",
              "awaiting_sign_off",
              "completed",
              "checklist_open",
              "no_show",
              "cancelled"
            ],
            "description": "The seat's state as the web roster words it: `status` with two derived states — `checklist_open`, a finalized seat whose course still waits on its sign-off checklist (it reads `completed` once every item is signed), and `awaiting_sign_off`, attendance whose proof is still owed. `status` stays the stored value.",
            "example": "checklist_open"
          },
          "roster_status_label": {
            "type": "string",
            "description": "The words for `roster_status` (\"Checklist open\", \"Completed\", …).",
            "example": "Checklist open"
          },
          "formatted_date": {
            "type": "string",
            "nullable": true,
            "description": "The held session's date, composed server-side in the VENUE's zone and ready to print. Pair with `formatted_time`.",
            "example": "Mon, Jul 20"
          },
          "formatted_time": {
            "type": "string",
            "nullable": true,
            "description": "The held session's time range WITH its zone abbreviation, ready to print — the same string a session row carries, so one renderer serves both.",
            "example": "9:00 AM – 1:00 PM CDT"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The instant, carrying the VENUE's UTC offset (not `Z`, and not the caller's). For sorting, countdowns and calendar entries; for display use `formatted_time`. Corrected 2026-09-16 — it previously serialized in the ambient request zone, so it was documented UTC and was in fact the caller's offset.",
            "example": "2026-07-20T09:00:00-05:00"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "As `starts_at` — the VENUE's offset."
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "IANA identifier for the session's zone, e.g. `America/Chicago` — resolvable by `TimeZone(identifier:)`, `ZoneId.of` and `Intl.DateTimeFormat`. Not needed for display; needed for a calendar entry's `TZID` and for arithmetic crossing a daylight-saving boundary, which an offset alone cannot support. Normalized on the way out as of 2026-09-16 — the stored value is an ActiveSupport zone name, so this previously returned unresolvable strings like `International Date Line West`."
          },
          "timezone_label": {
            "type": "string",
            "nullable": true,
            "description": "Short form for humans, e.g. `CDT`. REDUNDANT — the same text closes `formatted_time`. Retained for compatibility; do not build on it."
          },
          "duration_minutes": {
            "type": "integer",
            "nullable": true,
            "description": "Derived from the two instants; zone-independent."
          }
        }
      },
      "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 · 42m 00s 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 5m`. 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. Null too for an API token without destructive:training, which the restart asks.",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "restart"
                    ],
                    "example": "restart"
                  },
                  "label": {
                    "type": "string",
                    "example": "Start over"
                  }
                }
              }
            }
          }
        }
      },
      "UploadSession": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "composing",
              "completed",
              "failed",
              "expired"
            ],
            "description": "`open` (accepting parts) → `composing` (assembling) → `completed` or\n`failed`. `expired` is terminal too: the session ran past\n`expires_at` before it finished, or had failed and was kept past it.\n"
          },
          "filename": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer",
            "format": "int64"
          },
          "part_size": {
            "type": "integer",
            "description": "Bytes per part (the last part carries the remainder)."
          },
          "parts_count": {
            "type": "integer"
          },
          "uploaded_parts": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Part numbers stored so far (while `open`)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "result": {
            "type": "object",
            "additionalProperties": true,
            "description": "Present once `completed` — what the door made of the file."
          },
          "error": {
            "type": "string",
            "description": "Present when `failed`, and kept on an `expired` session that had failed."
          }
        }
      },
      "UsersDirectoryRow": {
        "type": "object",
        "description": "One person. A field the caller's directory hides is null (`locations` is then an empty list); `uid` and `id` are always present.",
        "properties": {
          "uid": {
            "type": "string",
            "nullable": true,
            "description": "The employee UID (`mango_employee_id`), the key `GET /users/{id}` takes.",
            "example": "E1234567"
          },
          "id": {
            "type": "integer"
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "first_name": {
            "type": "string",
            "nullable": true
          },
          "last_name": {
            "type": "string",
            "nullable": true
          },
          "full_name": {
            "type": "string"
          },
          "active": {
            "type": "boolean",
            "description": "The person's membership of this business is active."
          },
          "job_title": {
            "type": "string",
            "nullable": true
          },
          "department": {
            "type": "string",
            "nullable": true
          },
          "organizational_role": {
            "type": "string",
            "nullable": true
          },
          "hire_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "original_hire_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "employment_status": {
            "type": "string",
            "nullable": true
          },
          "employee_type": {
            "type": "string",
            "nullable": true,
            "description": "The older label derived from `employment_status`, kept for existing clients."
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "manager": {
            "type": "object",
            "nullable": true,
            "description": "The person's manager in this business.",
            "properties": {
              "uid": {
                "type": "string",
                "nullable": true
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "description": "Only where the caller's directory shows email."
              }
            }
          },
          "locations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "address": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "marketplace_apps": {
            "type": "array",
            "description": "The apps enabled for the business (the same list on every row).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "category": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "termination_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "termination_type": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_activity_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true,
            "description": "An absolute URL — the photo, or a generated initials image."
          },
          "avatar_updated_at": {
            "type": "integer",
            "nullable": true,
            "description": "When the photo was set, in Unix seconds — a cache-busting version; null with no photo."
          },
          "online": {
            "type": "boolean",
            "description": "Connected to chat (an idle person is `true`)."
          },
          "presence": {
            "type": "string",
            "enum": [
              "online",
              "offline"
            ]
          },
          "presence_state": {
            "type": "string",
            "enum": [
              "online",
              "idle",
              "offline"
            ]
          }
        }
      },
      "WebhooksWebhook": {
        "type": "object",
        "description": "A registered webhook. Never carries its signing secret.",
        "required": [
          "id",
          "business_id",
          "url",
          "description",
          "event_types",
          "active",
          "last_triggered_at",
          "consecutive_failures",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "description": "Where events are POSTed.",
            "example": "https://hooks.example.com/workforce"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "The webhook's label, shown in Admin → Webhooks. A webhook has no separate name."
          },
          "event_types": {
            "type": "array",
            "description": "The event names this webhook receives, or `\"*\"`. A name kept from before (a disabled plugin's event, an event whose platform switch is now off) stays listed and receives nothing until it can.",
            "items": {
              "type": "string"
            },
            "example": [
              "leave_request.approved",
              "shift.created"
            ]
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether deliveries are sent. Only `true` receives events; null (stored when an empty value was sent) is off. Switched off automatically after five deliveries in a row fail all their attempts."
          },
          "last_triggered_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a delivery last succeeded."
          },
          "consecutive_failures": {
            "type": "integer",
            "description": "Real deliveries in a row that failed every attempt (a failed test is not counted); reset by a success."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhooksWebhookWithSecret": {
        "description": "The webhook as POST /webhooks returns it — the only response with the secret.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhooksWebhook"
          },
          {
            "type": "object",
            "required": [
              "secret_key"
            ],
            "properties": {
              "secret_key": {
                "type": "string",
                "description": "The HMAC-SHA256 key behind every delivery's X-Webhook-Signature (\"sha256=\" + hex HMAC of the raw body). Generated as 40 hex characters unless the body set one."
              }
            }
          }
        ]
      },
      "WebhooksWebhookInput": {
        "type": "object",
        "description": "The fields a create or update may set. Any other key is ignored.",
        "properties": {
          "url": {
            "type": "string",
            "description": "A full http:// or https:// address with a host, no credentials, no"
          },
          "description": {
            "type": "string",
            "description": "The webhook's label."
          },
          "name": {
            "type": "string",
            "description": "Alias for `description`, used only when `description` is not sent."
          },
          "event_types": {
            "type": "array",
            "description": "Names from GET /webhooks/event_types (`event_types`, or a `plugin_event_types[].event_type`), or `[\"*\"]`. Must be an array; a bare string is dropped.",
            "items": {
              "type": "string"
            }
          },
          "active": {
            "type": "boolean",
            "nullable": true,
            "description": "Defaults to true on create. On update, anything that does not read as true switches the webhook off (and asks `destructive:webhooks` when it was on)."
          },
          "secret_key": {
            "type": "string",
            "description": "Your own signing secret. Generated when omitted on create; never echoed back by an update."
          }
        }
      },
      "WebhooksListMeta": {
        "type": "object",
        "required": [
          "current_page",
          "total_pages",
          "total_count"
        ],
        "properties": {
          "current_page": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "total_count": {
            "type": "integer",
            "description": "Active webhooks in the business."
          }
        }
      },
      "WebhooksValidationMessages": {
        "type": "object",
        "description": "A refused save, as sentences a person can be shown.",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "URL must be a full http:// or https:// address, such as https://example.com/webhook"
            ]
          }
        }
      },
      "WebhooksVerifyResult": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "response_code": {
            "type": "integer",
            "description": "The receiver's HTTP status. Absent when no response came back (the guard refused the URL, or the connection failed).",
            "example": 200
          },
          "error": {
            "type": "string",
            "description": "Why verification failed; absent on success.",
            "example": "HTTP 500"
          }
        }
      },
      "WebhooksTestResult": {
        "type": "object",
        "required": [
          "success",
          "delivery",
          "retry"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "retry": {
            "type": "boolean",
            "description": "Always false here — a test makes one attempt."
          },
          "error": {
            "type": "string",
            "description": "Why the delivery failed; absent on success.",
            "example": "HTTP 500"
          },
          "delivery": {
            "$ref": "#/components/schemas/WebhooksDelivery"
          }
        }
      },
      "WebhooksDelivery": {
        "type": "object",
        "description": "The delivery record the test wrote (every column of the row).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "webhook_id": {
            "type": "integer"
          },
          "business_id": {
            "type": "integer"
          },
          "event_type": {
            "type": "string",
            "example": "test.event"
          },
          "event_id": {
            "type": "string",
            "format": "uuid",
            "description": "Also sent as the X-Webhook-Event-Id header."
          },
          "payload": {
            "type": "object",
            "description": "The JSON body that was POSTed.",
            "properties": {
              "event_id": {
                "type": "string",
                "format": "uuid"
              },
              "event_type": {
                "type": "string"
              },
              "business_id": {
                "type": "integer"
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "data": {
                "type": "object",
                "description": "For a test, `message`, `timestamp` and `test_id`."
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "failed"
            ]
          },
          "response_code": {
            "type": "integer",
            "nullable": true
          },
          "response_body": {
            "type": "string",
            "nullable": true,
            "description": "The receiver's response body, cut to 5,000 characters."
          },
          "error_message": {
            "type": "string",
            "nullable": true
          },
          "attempted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "delivered_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "attempt_number": {
            "type": "integer"
          },
          "retry_count": {
            "type": "integer"
          },
          "max_retries": {
            "type": "integer",
            "description": "0 for a test."
          },
          "next_retry_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_retry_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "response_time_ms": {
            "type": "string",
            "nullable": true,
            "description": "Milliseconds the receiver took, as a decimal string (e.g. \"142.35\")."
          },
          "request_url": {
            "type": "string",
            "description": "The URL called, with any user:password@ replaced by [REDACTED]."
          },
          "request_headers": {
            "type": "string",
            "nullable": true,
            "description": "The request headers sent (Content-Type, User-Agent, X-Webhook-Signature, X-Webhook-Event-Id), stored as text."
          },
          "request_body_size": {
            "type": "integer",
            "nullable": true
          },
          "attempt_log": {
            "type": "array",
            "description": "One entry per attempt (at most 20), oldest first.",
            "items": {
              "type": "object",
              "properties": {
                "attempt": {
                  "type": "integer"
                },
                "attempted_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "success",
                    "failed"
                  ]
                },
                "response_code": {
                  "type": "integer"
                },
                "error": {
                  "type": "string"
                },
                "response_time_ms": {
                  "type": "number"
                },
                "redirected_to": {
                  "type": "string",
                  "description": "Where a cross-origin redirect ended (that hop is sent unsigned)."
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WeeklyReport": {
        "type": "object",
        "description": "A person's report for one period, with its lines by section.",
        "required": [
          "id",
          "status",
          "status_label",
          "period_start",
          "period_end",
          "due_at",
          "sections",
          "web_url"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 311
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "submitted",
              "skipped"
            ],
            "description": "`submitted` is shown as \"Shared\". Switch on this key; show `status_label`.",
            "example": "draft"
          },
          "status_label": {
            "type": "string",
            "example": "Draft"
          },
          "period_start": {
            "type": "string",
            "format": "date",
            "example": "2026-10-26"
          },
          "period_end": {
            "type": "string",
            "format": "date",
            "example": "2026-11-01"
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-30T17:00:00-07:00"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "mood": {
            "type": "string",
            "nullable": true,
            "enum": [
              "on_track",
              "at_risk",
              "blocked",
              null
            ],
            "description": "The writer's mood for the week; null when not picked.",
            "example": "on_track"
          },
          "mood_label": {
            "type": "string",
            "nullable": true,
            "description": "The mood in words (\"Smooth sailing\", \"A few bumps\", \"Stuck\").",
            "example": "Smooth sailing"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "default",
              "managers_only",
              "everyone"
            ],
            "example": "default"
          },
          "visibility_label": {
            "type": "string",
            "description": "Who reads it, in words, as the web says it.",
            "example": "Your team and your managers"
          },
          "skip_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "leave",
              "other",
              null
            ],
            "description": "Why a skipped report was skipped: `leave` — away all period (\"On leave\"); `other` — its writer skipped it (\"You skipped this one\"). Null unless skipped.",
            "example": null
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a shared report was last changed."
          },
          "sections": {
            "type": "array",
            "description": "The template's sections, in order, each with its lines.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "title",
                "kind",
                "items"
              ],
              "properties": {
                "key": {
                  "type": "string",
                  "example": "done"
                },
                "title": {
                  "type": "string",
                  "example": "Done"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "bullets",
                    "grouped",
                    "table",
                    "personal"
                  ],
                  "example": "bullets"
                },
                "items": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeeklyReportItem"
                  }
                }
              }
            }
          },
          "web_url": {
            "type": "string",
            "description": "The report's page on the web (relative path).",
            "example": "/apps/weekly-reports/reports/311"
          }
        }
      },
      "WeeklyReportItem": {
        "type": "object",
        "required": [
          "id",
          "section_key",
          "text",
          "position"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 812
          },
          "section_key": {
            "type": "string",
            "example": "help"
          },
          "text": {
            "type": "string",
            "example": "Need a second reviewer for the vendor contract"
          },
          "position": {
            "type": "integer",
            "example": 1
          },
          "plan_outcome": {
            "type": "string",
            "nullable": true,
            "enum": [
              "open",
              "done",
              "carried",
              "dropped",
              null
            ],
            "description": "A plan line's outcome (\"Planned\", \"Done\", \"Still going\", \"Let it go\"); null outside a plan section."
          },
          "help_status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WeeklyReportHelpStatus"
              }
            ],
            "nullable": true,
            "description": "An ask's status; null outside the \"Could use a hand with\" section."
          }
        }
      },
      "WeeklyReportHelpStatus": {
        "type": "string",
        "enum": [
          "open",
          "offered",
          "sorted"
        ],
        "description": "`open` — no offers yet; `offered` — someone offered to help; `sorted` — the writer marked it sorted.",
        "example": "offered"
      },
      "WeeklyReportHelpOffer": {
        "type": "object",
        "required": [
          "item_id",
          "help_status",
          "new_offer",
          "writer"
        ],
        "properties": {
          "item_id": {
            "type": "integer",
            "example": 812
          },
          "help_status": {
            "$ref": "#/components/schemas/WeeklyReportHelpStatus"
          },
          "new_offer": {
            "type": "boolean",
            "description": "True when this call recorded the offer (and told the writer); false when the caller had already offered.",
            "example": true
          },
          "writer": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "description": "The ask's writer — whom a client opens a Chat message to.",
            "properties": {
              "id": {
                "type": "integer",
                "example": 77
              },
              "name": {
                "type": "string",
                "example": "Priya Shah"
              }
            }
          }
        }
      },
      "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,
            "description": "Null where the organization's directory hides email from the caller."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "The person's name; for a person with no name, their email where it may be shown, else \"Unnamed member\"."
          }
        }
      },
      "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
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "description": "What sort of workspace this is — one of the values listed on `POST /workspace/workspaces` `kind`. Null when unclassified."
          },
          "kind_label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label for `kind`, e.g. \"Client account\". Null when `kind` is."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The workspace's tags. `[]` when none."
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The date the workspace's work ends, if one is set."
          },
          "series_key": {
            "type": "string",
            "nullable": true,
            "description": "Shared by every workspace in the same recurring series (set when a workspace is rolled forward into its next edition). Null for a workspace that is not part of a series."
          },
          "series_cadence": {
            "type": "string",
            "nullable": true,
            "enum": [
              "yearly",
              "quarterly",
              "monthly",
              null
            ]
          },
          "location_id": {
            "type": "integer",
            "nullable": true,
            "description": "The location this workspace belongs to, for a location workspace."
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The date the project starts, if one is set."
          },
          "health": {
            "type": "string",
            "enum": [
              "on_track",
              "at_risk",
              "off_track",
              "not_set"
            ],
            "description": "The owner's own rating of how the project is going; `not_set` until rated."
          },
          "organizational_department_id": {
            "type": "integer",
            "nullable": true,
            "description": "The department the project belongs to, if one is set."
          },
          "sponsor_id": {
            "type": "integer",
            "nullable": true,
            "description": "The user id of the project's sponsor, if one is set."
          },
          "provisioned_by": {
            "type": "string",
            "enum": [
              "manual",
              "location_sync",
              "import",
              "directory_sync"
            ],
            "description": "How the workspace was created — by a person, or by one of the automatic provisioners."
          },
          "owner_id": {
            "type": "integer",
            "description": "The workspace's primary owner — its creator, or whoever it was last handed to. A workspace can have SEVERAL owners: every member whose membership role is `owner` holds the same rights (manage members, review held posts, archive, export, delete), so the caller's own `role` — not this id — says whether they own it."
          },
          "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."
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "description": "Stable handle for the template. Accepted by `GET /workspace/templates/{id}` and by `template` on `POST /workspace/workspaces`. Unique among the system set only — a tenant's own template may reuse a system slug, in which case the system template wins a slug lookup and the numeric `id` is the unambiguous handle."
          },
          "kind": {
            "type": "string",
            "description": "The workspace `kind` a workspace created from this template starts with (the caller may override it on create). One of the values listed on `POST /workspace/workspaces` `kind`.",
            "example": "project"
          },
          "kind_label": {
            "type": "string",
            "description": "Human-readable label for `kind`, e.g. \"Client account\"."
          },
          "industries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Industries the template gallery files it under. `[]` when none."
          },
          "use_cases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Use cases the template is written for. `[]` when none."
          },
          "featured": {
            "type": "boolean",
            "description": "True when the template is featured in the gallery."
          },
          "hero_summary": {
            "type": "string",
            "nullable": true,
            "description": "The gallery's one-paragraph pitch for the template."
          },
          "structure": {
            "type": "object",
            "description": "The blueprint a workspace created from this template receives — sections, task lists and seed content. `{}` when the template defines none."
          },
          "setup": {
            "type": "object",
            "description": "What `POST /workspace/workspaces` with this template asks for.",
            "properties": {
              "scheduled": {
                "type": "boolean",
                "description": "True when the template's to-dos carry day offsets — send `anchor_date` to date them."
              },
              "anchor_label": {
                "type": "string",
                "description": "What the anchor date is, e.g. \"Go-live date\", \"Opening day\"."
              },
              "roles": {
                "type": "array",
                "description": "The template's role slots, filled with `roles` on create.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string",
                      "nullable": true
                    },
                    "defaults_to_creator": {
                      "type": "boolean",
                      "description": "True when an unsent slot is held by the caller."
                    }
                  }
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceTemplatePlan": {
        "type": "object",
        "description": "A template's dated plan, as `POST /workspace/workspaces` would write it.",
        "properties": {
          "scheduled": {
            "type": "boolean"
          },
          "anchor_label": {
            "type": "string"
          },
          "anchor_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "description": {
                  "type": "string",
                  "nullable": true
                },
                "defaults_to_creator": {
                  "type": "boolean"
                }
              }
            }
          },
          "rows": {
            "type": "array",
            "description": "Every to-do and milestone, earliest first; undated rows last.",
            "items": {
              "type": "object",
              "properties": {
                "section": {
                  "type": "string",
                  "description": "The task list it lands in (\"Milestones\" for milestones)."
                },
                "title": {
                  "type": "string"
                },
                "milestone": {
                  "type": "boolean"
                },
                "role": {
                  "type": "string",
                  "nullable": true,
                  "description": "The role slot key that owns it."
                },
                "start_offset_days": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Days from the anchor to its start (negative = before)."
                },
                "due_offset_days": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Days from the anchor to its due day (negative = before)."
                },
                "start_date": {
                  "type": "string",
                  "format": "date",
                  "nullable": true
                },
                "due_date": {
                  "type": "string",
                  "format": "date",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "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 external digest and external access when true."
          },
          "pinned": {
            "type": "boolean"
          },
          "kind": {
            "type": "string",
            "enum": [
              "standard",
              "announcement",
              "weekly_recap",
              "closeout_recap",
              "meeting_notes",
              "status_update"
            ],
            "description": "A `status_update` is an owner's project status update (PM-3)."
          },
          "health": {
            "type": "string",
            "nullable": true,
            "enum": [
              "on_track",
              "at_risk",
              "off_track",
              null
            ],
            "description": "The health the owner gave the project in a status update; null on every other kind."
          },
          "visible_to": {
            "type": "string",
            "enum": [
              "members",
              "owners"
            ],
            "description": "Who may read the post: `members` (everyone in the workspace) or `owners` (leads only — the workspace owners and Workspace app admins; absent for everyone else)."
          },
          "cascaded_from_id": {
            "type": "integer",
            "nullable": true,
            "description": "When this post is a cascaded copy, the id of the post it was copied from (e.g. a district post cascaded to its stores). Null for an original post, or when the source post has since been deleted."
          },
          "review_status": {
            "type": "string",
            "enum": [
              "published",
              "pending_review",
              "rejected"
            ],
            "description": "Posting-policy state. `published` is live. `pending_review` is held for the workspace owner (or a Workspace app admin) to approve; `rejected` was turned down with `review_notes`. A held post is readable only by its author and the reviewers."
          },
          "review_notes": {
            "type": "string",
            "nullable": true,
            "description": "The reviewer's reason while the post is `rejected`; null otherwise (it is cleared on approval and when the author's edit sends the post back for review)."
          },
          "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 the detail, create, edit, approve/reject and attachment 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"
          },
          "last_reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When someone last marked the post as still accurate (`POST …/messages/{id}/mark-reviewed`); `null` until then. A check never moves `updated_at`."
          }
        }
      },
      "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 Workspace external access 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_assisted": {
            "type": "boolean",
            "description": "True when the task was proposed by the Workspace agent and a person accepted it — the platform's AI-assisted mark. Told to the caller under the tenant's reader setting (\"Show readers when AI helped write a message\", a company-wide setting); the person who added the task and the workspace's owners and admins always see it."
          },
          "ai_drafted": {
            "type": "boolean",
            "deprecated": true,
            "description": "The old name of `ai_assisted`, with the same value. Removed after 2026-10-15."
          },
          "internal_only": {
            "type": "boolean"
          },
          "recurring_cadence": {
            "type": "string",
            "nullable": true
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "important",
              "critical"
            ]
          },
          "estimated_duration_minutes": {
            "type": "integer",
            "nullable": true
          },
          "milestone": {
            "type": "boolean"
          },
          "parent_task_id": {
            "type": "integer",
            "nullable": true,
            "description": "Set on a subtask — the to-do it sits under."
          },
          "unfilled_role": {
            "type": "string",
            "nullable": true,
            "description": "The project template's role slot this to-do was made for (\"IT lead\") while nobody is assigned; null once someone is."
          },
          "collaborator_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The collaborators beside the one accountable owner (`assignee`)."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "This workspace's custom to-do fields, `field_name` → stored value."
          },
          "waits_on": {
            "type": "array",
            "description": "The to-dos this one waits on (PM-2a, finish-to-start).",
            "items": {
              "type": "object",
              "properties": {
                "dependency_id": {
                  "type": "integer"
                },
                "task_id": {
                  "type": "integer",
                  "description": "The to-do it waits on."
                },
                "lag_days": {
                  "type": "integer",
                  "description": "Days after that one finishes; negative overlaps."
                }
              }
            }
          },
          "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"
          },
          "kind": {
            "type": "string",
            "enum": [
              "question",
              "status_update"
            ],
            "description": "`status_update` is the owner-only reminder to post a project status update (PM-3); its answer is posted as a status update."
          },
          "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
          },
          "time_of_day": {
            "type": "string",
            "nullable": true,
            "description": "The chosen opening time, 24-hour `HH:MM`, read in `timezone`; null when the check-in opens at the hour it was created."
          },
          "days_of_week": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "For a weekly check-in with a chosen time, the weekdays it asks on (0 = Sunday … 6 = Saturday)."
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "The time zone `time_of_day` is read in."
          },
          "answer_type": {
            "type": "string",
            "enum": [
              "text",
              "select",
              "boolean",
              "number",
              "date"
            ],
            "description": "What members answer with. `text` is a written answer (every check-in created before typed answers, and the status-update reminder, always). The typed ones — `select` (pick one from `answer_options.choices`), `boolean` (yes / no), `number` and `date` — are tallied per cycle on the check-in page and are never AI-drafted."
          },
          "answer_options": {
            "type": "object",
            "description": "The answer type's settings: `choices` for `select`, the optional `min` / `max` for `number` (1 and 5 make a rating), empty for the other types.",
            "properties": {
              "choices": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The options members pick from, in order (2–20)."
              },
              "min": {
                "type": "number",
                "description": "The lowest number allowed, when set."
              },
              "max": {
                "type": "number",
                "description": "The highest number allowed, when set."
              }
            }
          },
          "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
          },
          "answer_value": {
            "type": "string",
            "nullable": true,
            "description": "A typed answer's value. Always null here — only check-ins that take a written answer (`answer_type` `text`) are drafted."
          },
          "drafted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ai_assisted": {
            "type": "boolean",
            "description": "Always true here — every draft is one the Workspace agent wrote."
          },
          "ai_drafted": {
            "type": "boolean",
            "deprecated": true,
            "description": "The old name of `ai_assisted`, with the same value. Removed after 2026-10-15."
          }
        }
      },
      "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",
            "description": "For an all-day event, local midnight of its date in the caller's time zone; read start_date for the date itself."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The calendar date of an all-day event (null for timed events)."
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "The inclusive last date of an all-day event (null for timed events)."
          },
          "internal_only": {
            "type": "boolean",
            "description": "Excluded from the external digest and external access 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"
          }
        }
      },
      "WorkspaceHealthSignals": {
        "type": "object",
        "description": "What the data shows about a project (PM-3), beside the owner's own `health`.",
        "properties": {
          "overdue_tasks": {
            "type": "integer"
          },
          "check_in_due": {
            "type": "boolean",
            "description": "The owner has not answered this cycle's status-update check-in."
          },
          "missed_milestones": {
            "type": "integer"
          },
          "open_risks": {
            "type": "integer"
          },
          "high_risks": {
            "type": "integer",
            "description": "Open risks rated high or critical."
          },
          "tone": {
            "type": "string",
            "description": "The overall tone the signals give (for a badge)."
          }
        }
      },
      "WorkspaceCustomField": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "field_name": {
            "type": "string",
            "description": "The key a to-do's `custom_fields` uses."
          },
          "label": {
            "type": "string"
          },
          "field_type": {
            "type": "string",
            "enum": [
              "text",
              "long_text",
              "number",
              "currency",
              "date",
              "select",
              "boolean",
              "email",
              "url",
              "user"
            ]
          },
          "choices": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "A `select` field's options; null for every other type."
          },
          "position": {
            "type": "integer"
          }
        }
      },
      "WorkspaceRisk": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "workspace_id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "enum": [
              "safety",
              "quality",
              "environmental",
              "security",
              "operational",
              "financial",
              "legal",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "identified",
              "assessing",
              "mitigating",
              "monitoring",
              "accepted",
              "closed"
            ]
          },
          "likelihood": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "impact": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "score": {
            "type": "integer",
            "description": "likelihood × impact."
          },
          "rating": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical"
            ]
          },
          "owner_id": {
            "type": "integer",
            "nullable": true
          },
          "mitigation_plan": {
            "type": "string",
            "nullable": true
          },
          "review_frequency": {
            "type": "string",
            "nullable": true,
            "description": "Returned only where Compliance Hub is enabled for the business.",
            "enum": [
              "monthly",
              "quarterly",
              "semiannual",
              "annual",
              "ad_hoc",
              null
            ]
          },
          "review_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Returned only where Compliance Hub is enabled for the business."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceRiskInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "safety",
              "quality",
              "environmental",
              "security",
              "operational",
              "financial",
              "legal",
              "other"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "identified",
              "assessing",
              "mitigating",
              "monitoring",
              "accepted",
              "closed"
            ]
          },
          "likelihood": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "impact": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "owner_id": {
            "type": "integer",
            "description": "An active member of the project."
          },
          "mitigation_plan": {
            "type": "string"
          },
          "review_on": {
            "type": "string",
            "format": "date"
          },
          "review_frequency": {
            "type": "string",
            "enum": [
              "monthly",
              "quarterly",
              "semiannual",
              "annual",
              "ad_hoc"
            ]
          }
        }
      },
      "WorkspaceDependency": {
        "type": "object",
        "description": "A finish-to-start link (PM-2a) — the successor may start `lag_days` after the predecessor finishes.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "successor_id": {
            "type": "integer",
            "description": "The to-do that waits."
          },
          "successor_title": {
            "type": "string"
          },
          "predecessor_id": {
            "type": "integer",
            "description": "The to-do it waits on."
          },
          "predecessor_title": {
            "type": "string"
          },
          "lag_days": {
            "type": "integer",
            "description": "Negative lets the two overlap."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceScheduleConflict": {
        "type": "object",
        "description": "A to-do that starts before a to-do it waits on has finished (plus the gap).",
        "properties": {
          "dependency_id": {
            "type": "integer"
          },
          "predecessor_id": {
            "type": "integer"
          },
          "predecessor_title": {
            "type": "string"
          },
          "successor_id": {
            "type": "integer"
          },
          "successor_title": {
            "type": "string"
          },
          "lag_days": {
            "type": "integer"
          },
          "earliest_start": {
            "type": "string",
            "format": "date",
            "description": "The first day the successor may start."
          },
          "days_early": {
            "type": "integer",
            "description": "How many days before that it starts now."
          },
          "movable": {
            "type": "boolean",
            "description": "False when the successor has already started — a shift leaves it where it is."
          }
        }
      },
      "WorkspaceScheduleMove": {
        "type": "object",
        "description": "One to-do moved later, whole, by a shift.",
        "properties": {
          "task_id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "days": {
            "type": "integer"
          },
          "start_from": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "start_to": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "due_from": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "due_to": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "subtasks": {
            "type": "array",
            "description": "The to-do's open, not-started, dated subtasks, moved by the same days.",
            "items": {
              "$ref": "#/components/schemas/WorkspaceScheduleMove"
            }
          }
        }
      },
      "WorkspaceTimelineTask": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "overdue",
              "completed"
            ]
          },
          "completed": {
            "type": "boolean"
          },
          "milestone": {
            "type": "boolean"
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "important",
              "critical"
            ]
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "due_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Set when the to-do is due on a day rather than at a time."
          },
          "starts_on": {
            "type": "string",
            "format": "date",
            "description": "The first day of its span."
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "description": "The last day of its span."
          },
          "assignee": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "waits_on": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "dependency_id": {
                  "type": "integer"
                },
                "task_id": {
                  "type": "integer"
                },
                "lag_days": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "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": "People Changes",
      "description": "HR write-back receiver API. A write-back webhook (event people.change) carries only a reference to an approved profile change; the receiving middleware fetches the change with read:people_changes and reports whether it was applied with write:people_changes. Administrator tokens only."
    },
    {
      "name": "Upload Sessions",
      "description": "Chunked, resumable uploads for large files (over 200 MB) into Drive, AI Notepad and Recorder. Every byte goes through the API in bounded parts — no presigned URLs — so a dropped connection costs one part, not the file."
    },
    {
      "name": "Branding",
      "description": "The business's theme tokens, logo, brand fonts and colour palette, for a client that themes itself at runtime."
    },
    {
      "name": "Tasks",
      "description": "Personal tasks for One Mango's own clients — the JSON twin of the web's My Tasks quick-create drawer. Always the caller's own task."
    },
    {
      "name": "Desktop",
      "description": "OneMango Desktop — the one boot answer (rail, every app's boot block, branding) the desktop app draws itself from right after login. Composed from the same builders the per-app endpoints render, so the two cannot drift; never 403s (an empty landing is the \"nothing is switched on for the desktop\" answer)."
    },
    {
      "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 external 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": "Lone Worker",
      "description": "Lone Worker — a worker's own safety status, SOS, floor-issue report and check-in answer, and the responder's alert feed with acknowledge and resolve. Reads need read:lone_worker, writes write:lone_worker (the own_ forms are accepted)."
    },
    {
      "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": "AI Interviews",
      "description": "The caller's own conversations with the AI interviewer — Offboarding's \"Talk it through\", Surveys' conversation mode, Shift Feedback's check-in — action for action with the conversation page: start or resume through the app's own door, the opening (the consent), typed, tapped and spoken answers, skip, the review (\"Here's what we'll record\"), send and delete. Named conversations only: an anonymous one is held on the web (`anonymous_on_web`). Live voice is a browser transport and is not offered here. Reads need read:ai_interviews, writes write:ai_interviews (the own_ forms are accepted; the app's sign-in tokens carry them)."
    },
    {
      "name": "Ideas",
      "description": "Ideas — dashboard, lists and search, idea detail, votes, comments, scores, stage moves and campaigns. The API token's scope (owner decision 2026-10-06, #6720): `read:ideas` to read, `write:ideas` to change anything, and `destructive:ideas` to delete an idea, a campaign or a comment, close a campaign, or move an idea into a declined stage (on top of `write:ideas`) — and, also on top of `write:ideas`, to edit a running campaign's close date to a day before today, which closes it (#7186). The sign-in tokens hold all three."
    },
    {
      "name": "Comms Hub",
      "description": "Communications (Comms Hub / News Feed) — the home dashboard (unread/ack/verified counts, open poll, latest posts, must-read status, trending topics), newsletters (publications and issues), audiences, and campaigns (the Campaign field's choices, and putting a message in one)"
    },
    {
      "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, search, pages, comments, reactions and bookmarks. The API token's scope (owner decision 2026-10-04, #6720): `read:wikis` to read, `write:wikis` to change anything, and `destructive:wikis` to delete or archive a wiki or delete a comment. The sign-in tokens hold all three."
    },
    {
      "name": "Sites",
      "description": "Sites app — company intranet sites and their pages. The reader's own writes and lists: follow a site, watch a page, read and post a page's comments, and the caller's Saved & following lists (following, watching, recently read). POST only adds and DELETE only removes (never a toggle); DELETE is never gated on still having access, so nobody is stranded in a subscription to a site they lost."
    },
    {
      "name": "Search",
      "description": "One ranked search across everything the caller can see — the web results page (/apps/search) as data: the same engine, ranking, live answers, filters (In · When · Only mine) and permissions, plus the page before you type (saved and recent searches) when no query is given — and the caller's saved searches (save, list, remove), and the lookahead listed while the caller is still typing (the omnibox's lookahead)."
    },
    {
      "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": "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": "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": "My Team",
      "description": "What the caller's team still owes, rolled up per direct report (read:people_directory)"
    },
    {
      "name": "Share",
      "description": "Send a record the caller can open (a wiki, a Sites page, a Training course) to a chat room, a Teams or Slack channel, or a colleague, and take it back — the web Share drawer's rules."
    },
    {
      "name": "Shared Moments",
      "description": "A person shares their own recognition, award, work anniversary, contest win, certificate or year in recognition outside the company as a card and a link — list, share, stop (the moment's app's read/write scope)"
    },
    {
      "name": "Notifications",
      "description": "User notification management — mark read/unread, archive, bulk operations (mobile support). A token needs `read:notifications` for every GET (the list, one item, the counts, the grouped and home views) and `write:notifications` for every change (mark read or unread, archive, restore, snooze, dismiss, delete, reply) and for an inline action that is not a decision (acknowledge, sign-off statements); a decision (approve, reject, resume, sign) needs `write:approvals`. Without the scope the answer is 403 `insufficient_permissions`. The native app's sign-in tokens carry all three.\n"
    },
    {
      "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. Every operation answers 403 with `error.code` `app_not_enabled` / `app_not_accessible` when the caller has no Chat, and `insufficient_permissions` when the token lacks the scope: read:chat for GET, write:chat for changes, destructive:chat for final acts — deleting a message or a conversation, leaving one, removing a member, archiving a channel, closing a poll (read:own_chat / write:own_chat also satisfy the first two). An operation's own 403 list names only its own refusals."
    },
    {
      "name": "Weekly Reports",
      "description": "Weekly Reports — this period's report, a quick-added line, Skip, and \"I can help\" on a teammate's ask. Reads need `read:weekly_reports`, changes `write:weekly_reports`. Every operation answers 403 `app_disabled` / `app_forbidden` when the caller has no Weekly Reports."
    },
    {
      "name": "Meal Compliance",
      "description": "Meal Compliance — the Dashboard tab (waiver coverage, pending signatures, behavior alerts, employee withdrawals) and the Waivers register (status filters with counts, search, location, sort), and My Breaks (the caller's own waiver status, signing, break pattern and waiver history), and Break Compliance (the break-finding report — tiles, rules in force, All / Unresolved / Resolved findings)"
    },
    {
      "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. The API token's scope (owner decision 2026-10-06, #6720): `read:news_feed` to read, `write:news_feed` to post, comment, react, vote or upload, and `destructive:news_feed` to delete a post, a comment or a file, close a poll or a discussion, or mark a verified answer outdated. Featuring a post asks `read:/write:news_feed_editorial_features`, and comment reports `read:/write:news_feed_reports`. The sign-in tokens hold all three News Feed scopes; a token a person makes by hand holds read and write. After the scope, every News Feed endpoint (all but `/admin/news-feed/settings`) answers only where Communications is on for the business and open to the caller, under the admin's Communications access policy: otherwise 403 with `error.code` `app_disabled`, `app_unavailable` or `work_context_restricted`, before anything is read or written (#7173)."
    },
    {
      "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": "Webhooks",
      "description": "Register, test and manage the signed webhooks a business's events are delivered to (admin tokens holding the webhooks scopes)"
    },
    {
      "name": "Forecasts",
      "description": "Per-day, per-location staffing forecasts with hourly predictions — create, bulk-create, read by date range, update and delete"
    },
    {
      "name": "Demand Signals",
      "description": "Events, weather, holidays and promotions that change demand at a location on a date, and feed forecasts"
    },
    {
      "name": "Export Files",
      "description": "Background CSV/JSON/XLSX exports of people, shifts, assignments, attendance and leave — start one, poll its status, list and download with a signed link"
    },
    {
      "name": "Live Assist",
      "description": "Read-only Live Assist video sessions the caller may see — the list and one session"
    },
    {
      "name": "Policy Hub",
      "description": "Policies and handbooks published to the caller, their acknowledgements, and acknowledging a policy"
    },
    {
      "name": "SOP Hub",
      "description": "The caller's SOPs to complete, reading and working through a procedure step by step, and the manuals open to them"
    },
    {
      "name": "Locations",
      "description": "The caller's assigned work locations and a location's departments"
    },
    {
      "name": "Recipient Group Parameters",
      "description": "The lists a recipient group's send-time parameters are picked from (locations, departments, people), and which parameters a group needs"
    },
    {
      "name": "Practice Hub",
      "description": "Read-only Practice Hub — the business's published scenario library and the caller's own practice attempts"
    },
    {
      "name": "Muscle Memory",
      "description": "Muscle Memory — the lessons a business has learned. Read and search them, read the aggregate learning picture, add a lesson, and curate one: confirm it, correct it with a note, reinstate a superseded one, or forget it. Forgetting (DELETE) is permanent — the lesson is archived, never listed again and never re-learned, and nothing restores it. Scopes read:muscle_memory, write:muscle_memory and, for forgetting, destructive:muscle_memory."
    },
    {
      "name": "Asset Pro",
      "description": "One Asset Pro asset (equipment) for its holder, an Asset Pro admin or the holder's manager"
    },
    {
      "name": "Appointment Scheduler",
      "description": "The caller's booking pages and the appointments booked on them — list and read them, preview availability, and cancel or move an appointment"
    },
    {
      "name": "Bookings",
      "description": "Book rooms, desks and other spaces; list, change, cancel, check in and out of bookings, and read the bookable spaces"
    },
    {
      "name": "Office Integration",
      "description": "The API behind the MangoApps Word/Excel add-in — the manifest, the app picker, and finding, opening, downloading, revising and acting on records"
    },
    {
      "name": "Intelligence",
      "description": "The caller's AI-assistant personalisation (smart defaults, suggestions, usage patterns, feedback) and admin-only AI analytics"
    },
    {
      "name": "Employees",
      "description": "A colleague's directory card and a person's shift schedule"
    },
    {
      "name": "Recent Entities",
      "description": "The most recently updated employees, shifts or locations of the business, for quick pickers"
    },
    {
      "name": "Notes",
      "description": "Add a Performance (EPMS) note to a review, goal, 1:1 meeting, development plan or person"
    },
    {
      "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."
    },
    {
      "name": "AI Front Office",
      "description": "The dispatcher's booking-request review queue (decline)."
    },
    {
      "name": "Analytics",
      "description": "Workforce sentiment alerts (dismiss)."
    },
    {
      "name": "Bulk Operations",
      "description": "Queued batch writes to shifts, assignments, availabilities, preferences, demand signals and forecasts."
    },
    {
      "name": "Calendar",
      "description": "Calendar events (delete)."
    },
    {
      "name": "Classifieds",
      "description": "Classified listings (remove)."
    },
    {
      "name": "Mango Signal",
      "description": "Actions taken on engagement signals (update, dismiss)."
    },
    {
      "name": "Mango Spend",
      "description": "Expense reports and their approval (reject, remove a line)."
    },
    {
      "name": "Password Manager",
      "description": "Password entries (delete, archive)."
    },
    {
      "name": "Recorder",
      "description": "Screen recordings, their annotations, comments, shares and public links."
    },
    {
      "name": "Recruiting",
      "description": "Recruiting candidates (update status)."
    }
  ]
}