{
  "openapi": "3.0.3",
  "info": {
    "title": "Surventrics API",
    "description": "Survey management and analytics API for Surventrics. Create, manage, and analyze surveys programmatically.",
    "version": "1.0.0",
    "contact": {
      "name": "Surventrics Support",
      "url": "https://surventrics.ai/support"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://surventrics.ai/terms"
    }
  },
  "servers": [
    {
      "url": "https://surventrics.ai/api/v1",
      "description": "Production server"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Surveys",
      "description": "Survey management operations"
    },
    {
      "name": "Questions",
      "description": "Survey question operations"
    },
    {
      "name": "Responses",
      "description": "Survey response operations"
    },
    {
      "name": "Analytics",
      "description": "Survey analytics and metrics"
    },
    {
      "name": "Webhooks",
      "description": "Webhook subscription management (Business+ plan). Used by Zapier/Make REST hooks."
    },
    {
      "name": "Organisation",
      "description": "Folders, tags and user groups \u2014 how surveys are organised."
    },
    {
      "name": "Contacts",
      "description": "Contacts, lists and suppressions. Personal data: requires emailDistribution (Pro+) and its own scopes."
    },
    {
      "name": "Email",
      "description": "Email sends and sending domains. Read-only for now \u2014 see the notes on creating a send."
    },
    {
      "name": "Collectors",
      "description": "Distribution channels for a survey. Listing is free; creating and editing need namedCollectors."
    },
    {
      "name": "Variables",
      "description": "Static and computed survey variables. Reading is free; defining and editing need calculatedFields."
    },
    {
      "name": "Calibration",
      "description": "Priors, insignificance prediction, multi-arm status and the objective a survey optimises for."
    },
    {
      "name": "Instruments",
      "description": "Standardised instruments and adaptive probes. Both are feature-gated."
    },
    {
      "name": "Debriefs",
      "description": "Cognitive debriefing runs. Evidence is withheld below the anonymity floor."
    },
    {
      "name": "Rewards",
      "description": "Per-respondent voucher codes. Uploading codes needs a separate organisation permission."
    }
  ],
  "paths": {
    "/surveys": {
      "get": {
        "summary": "List surveys",
        "description": "Retrieve a paginated list of surveys for your organization.",
        "operationId": "listSurveys",
        "tags": ["Surveys"],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by survey status",
            "schema": {
              "type": "string",
              "enum": ["draft", "live", "paused", "closed"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of surveys",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Survey"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "post": {
        "summary": "Create survey",
        "description": "Create a new survey. The survey will be created in draft status.",
        "operationId": "createSurvey",
        "tags": ["Surveys"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSurveyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Survey created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Survey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}": {
      "get": {
        "summary": "Get survey",
        "description": "Retrieve a survey and, when the key carries questions:read, its questions. Localisation and translation status, calculated-field definitions and respondent access controls are returned only when requested via `include`.",
        "operationId": "getSurvey",
        "tags": ["Surveys"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks: localization, variables, access. Omitted blocks are absent from the response. An unrecognised block is a 400 rather than being ignored.",
            "schema": {
              "type": "string",
              "example": "localization,variables"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Survey details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SurveyWithQuestions"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "put": {
        "summary": "Update survey",
        "description": "Update an existing survey. You can update the title, description, messages, and status.",
        "operationId": "updateSurvey",
        "tags": ["Surveys"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSurveyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Survey updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Survey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      },
      "delete": {
        "summary": "Delete survey",
        "description": "Delete a survey. Live surveys cannot be deleted - pause or close them first.",
        "operationId": "deleteSurvey",
        "tags": ["Surveys"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "204": {
            "description": "Survey deleted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/questions": {
      "get": {
        "summary": "List questions",
        "description": "Retrieve all questions for a survey, ordered by position.",
        "operationId": "listQuestions",
        "tags": ["Questions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "List of questions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Question"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:read"]
      },
      "post": {
        "summary": "Create question",
        "description": "Add a new question to a survey. Cannot add questions to live surveys.",
        "operationId": "createQuestion",
        "tags": ["Questions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateQuestionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Question created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Question"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:write"]
      }
    },
    "/surveys/{surveyId}/questions/{questionId}": {
      "get": {
        "summary": "Get question",
        "description": "Retrieve a single question by ID.",
        "operationId": "getQuestion",
        "tags": ["Questions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "$ref": "#/components/parameters/questionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Question details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Question"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:read"]
      },
      "put": {
        "summary": "Update question",
        "description": "Update an existing question. Cannot modify questions on live surveys.",
        "operationId": "updateQuestion",
        "tags": ["Questions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "$ref": "#/components/parameters/questionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateQuestionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Question updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Question"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:write"]
      },
      "delete": {
        "summary": "Delete question",
        "description": "Remove a question from a survey. Cannot delete questions from live surveys.",
        "operationId": "deleteQuestion",
        "tags": ["Questions"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "$ref": "#/components/parameters/questionId"
          },
          {
            "name": "acknowledgeConsequence",
            "in": "query",
            "required": false,
            "description": "Set to `true` to remove a question from a LIVE survey. Only the exact string `true` counts as consent.",
            "schema": {
              "type": "string",
              "enum": ["true"]
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Question deleted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:write"]
      }
    },
    "/surveys/{surveyId}/responses": {
      "get": {
        "summary": "List responses",
        "description": "Retrieve a paginated list of survey responses with answers. Two pagination modes: legacy page mode (no `cursor`; pagination has total/page/pageSize/totalPages/hasMore plus an additive nextCursor for bootstrapping a cursor walk) and cursor mode (`cursor` present; pagination has limit/hasMore/nextCursor, no COUNT executed). `cursor` and `page` are mutually exclusive (400). `sort=completed_at` implies completed sessions and cannot be combined with a conflicting `status` (400).",
        "operationId": "listResponses",
        "tags": ["Responses"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (legacy page mode). Mutually exclusive with cursor.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Number of items per page (also the batch size in cursor mode)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by response status",
            "schema": {
              "type": "string",
              "enum": ["in_progress", "completed", "abandoned"]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field (ascending, id-tiebroken). `completed_at` only applies to completed sessions.",
            "schema": {
              "type": "string",
              "enum": ["started_at", "completed_at"],
              "default": "started_at"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Inclusive lower bound on the active sort field (sort=started_at \u2192 startedAt, sort=completed_at \u2192 completedAt), ISO 8601",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Inclusive upper bound on the active sort field, ISO 8601",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque cursor from a previous response's pagination.nextCursor. Enables cursor (keyset) mode. Malformed cursors return 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of responses. Page mode returns the standard paginated envelope; cursor mode returns the cursor-paginated envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/PaginatedResponse"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "data": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Response"
                              }
                            }
                          }
                        }
                      ]
                    },
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CursorPaginatedResponse"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "data": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Response"
                              }
                            }
                          }
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["responses:read"]
      }
    },
    "/surveys/{surveyId}/responses/export": {
      "get": {
        "summary": "Export responses",
        "description": "Export a survey's responses. The file is the same artefact the results page downloads: metadata columns followed by one column per question, headed by the question's text. csv and json are text; xlsx is an Excel workbook; spss is a ZIP containing data.csv, an import.sps syntax file that already points at it, and a README \u2014 open the syntax in SPSS and the variables arrive labelled.",
        "operationId": "exportResponses",
        "tags": ["Responses"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by completion status",
            "schema": {
              "type": "string",
              "enum": ["completed", "all"],
              "default": "completed"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Filter responses after this date (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Filter responses before this date (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "csv (default), json, xlsx or spss. An unrecognised format is a 400.",
            "schema": {
              "type": "string",
              "enum": ["csv", "json", "xlsx", "spss"],
              "default": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export, as an attachment. Content-Type is text/csv, application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, or application/zip.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["responses:export"]
      }
    },
    "/surveys/{surveyId}/analytics": {
      "get": {
        "summary": "Get analytics",
        "description": "Retrieve the analytics overview for a survey: session counts, completion rate, timing, per-question statistics and a daily series. The completion rate divides completions by QUALIFIED sessions (completed + abandoned + in progress); sessions a screener, a quota or a quality rule ended are reported separately and are not in the denominator. Device, geography, quality and confidence blocks are returned only when requested via `include`.",
        "operationId": "getAnalytics",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated optional blocks to include: devices, geography, quality, confidence. Omitted blocks are absent from the response. An unrecognised block is a 400 rather than being ignored.",
            "schema": {
              "type": "string",
              "example": "devices,geography"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Analytics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/analytics/questions": {
      "get": {
        "summary": "Get per-question analytics",
        "description": "Response distributions and statistics for every question in the survey, or for one named question.\n\nVisibility-aware: counts are against respondents who were actually shown the question, so a question a respondent was routed past by logic does not reduce the response rate of the people who saw it. `shownCount` is the base; `skippedCount` is respondents the logic never asked.\n\nWelcome and thank-you screens carry no analytics and are not returned. A `questionId` that names no analysed question returns 404 rather than an empty list \u2014 every analysed question is returned even with no data, so an empty result means the question is not here, which is a different statement from 'no answers yet'.",
        "operationId": "getQuestionAnalytics",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "questionId",
            "in": "query",
            "required": false,
            "description": "Narrow the result to a single question.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-question analytics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionAnalytics"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/analytics/quality": {
      "get": {
        "summary": "Get response quality",
        "description": "Data-quality summary, flag statistics, and the sessions that were flagged.\n\nThe flagged list is a page rather than the whole table. `limit` above the maximum of 200 is clamped, not refused, and the applied `limit` and `filter` are echoed in the body so a capped request says so on the wire.",
        "operationId": "getResponseQuality",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum flagged sessions to return. Defaults to 50, clamped to 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "description": "Which flagged sessions to return. An unrecognised value is refused rather than falling back to `all`.",
            "schema": {
              "type": "string",
              "default": "all",
              "enum": [
                "all",
                "speeding",
                "straightlining",
                "low_attention",
                "bot_suspected",
                "duplicate",
                "honeypot",
                "behavior_anomaly",
                "excluded",
                "high_risk"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Response quality",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResponseQuality"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/analytics/funnel": {
      "get": {
        "summary": "Get the drop-off funnel",
        "description": "The logic-aware drop-off funnel and the questions respondents most often leave on.\n\nA respondent routed past a question did not drop out there: `skippedByLogicCount` is reported separately from `abandonedCount`, and collapsing the two would invent abandonment that never happened.",
        "operationId": "getFunnelAnalysis",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Funnel analysis",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FunnelAnalysis"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/analytics/flow": {
      "get": {
        "summary": "Get the survey flow graph",
        "description": "The branching respondent-flow graph: nodes carrying visit counts, links carrying volume and whether a logic rule caused the transition, plus the logic-aware funnel, loop retention, dead ends and exit points.\n\nComposed from the same read the Funnel tab renders, so the numbers here and the numbers on screen cannot disagree. `loops` is `[]` for a survey without loops rather than absent.",
        "operationId": "getSurveyFlow",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Survey flow graph",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SurveyFlow"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/analytics/cross-tabulation": {
      "get": {
        "summary": "Cross-tabulate two dimensions",
        "description": "Completion and drop-off for every combination of two respondent dimensions.\n\nBoth dimensions are required and both are validated: a table built on a substituted dimension looks exactly like the answer to the question that was asked, and nothing downstream can tell the difference. `cells` is row-major and aligned to `rowLabels` and `colLabels`.",
        "operationId": "getCrossTabulation",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "rows",
            "in": "query",
            "required": true,
            "description": "The dimension down the side of the table.",
            "schema": {
              "type": "string",
              "enum": ["device", "browser", "geography", "question_type"]
            }
          },
          {
            "name": "cols",
            "in": "query",
            "required": true,
            "description": "The dimension across the top of the table.",
            "schema": {
              "type": "string",
              "enum": ["device", "browser", "geography", "question_type"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cross-tabulation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CrossTabulation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/pretest/compare": {
      "post": {
        "summary": "Compare two phrasings before fielding",
        "description": "Estimate the effect of a phrasing change on completion rate before collecting a single response, using a calibrated model prediction.\n\nThis endpoint calls a model, so it **consumes the organisation's monthly AI action allowance** (weight 2). The allowance is checked before the model is called and recorded only after the prediction succeeds, so a provider failure is not billed. Organisations supplying their own provider credentials are exempt \u2014 they are billed by the provider directly.\n\nExhausting the allowance returns **402 with `quota_exceeded`**, deliberately not 429: a monthly allowance does not clear on retry, and a client that treats it as a rate limit will loop against it. Separately, this endpoint is rate-limited to 10 requests per minute per organisation, which does return 429.\n\nRequires the `variantTesting` feature (Starter plan or above).",
        "operationId": "comparePretestPhrasings",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["phrasingA", "phrasingB", "category"],
                "properties": {
                  "phrasingA": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000
                  },
                  "phrasingB": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "question_wording",
                      "scale_labeling",
                      "question_order",
                      "response_options",
                      "survey_length",
                      "tone_formality",
                      "other"
                    ],
                    "description": "What kind of change is being compared."
                  },
                  "audience": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Who the survey is fielded to, if known."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Predicted effect of the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PretestPrediction"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"],
        "x-consumes-ai-allowance": {
          "kind": "pretest",
          "weight": 2
        }
      }
    },
    "/surveys/{surveyId}/insights": {
      "get": {
        "summary": "Get survey insights",
        "description": "Plain-language observations about a survey, the fixes they imply, and a 0-100 health score penalising low completion, low response quality and a thin sample.\n\n**Computed, not generated.** Every figure and sentence comes from a fixed threshold rule over already-computed analytics. No model is called and no AI action allowance is consumed \u2014 `method: \"computed\"` states this in the response, because the MCP tool this backs sits in a family named for AI and an agent relays whatever provenance it is given.",
        "operationId": "getSurveyInsights",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Insights and health score",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SurveyInsights"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/ask": {
      "post": {
        "summary": "Ask a question about the data",
        "description": "A plain-language question, answered from the survey's analytics.\n\n**Computed, not generated**, despite the verb. Two branches read already-computed results \u2014 instrument scores and probe transcripts, which exist precisely so a provider is never asked to compute a score that must be read \u2014 and everything else is keyword matching over aggregates. No model is called and no AI action allowance is consumed.\n\nThe answer carries its own caveats and they are not optional: `isDefensible` says whether the base supports the claim, `limitations` names what is wrong with it, and the answer text gains a readiness note when the survey has not reached statistical confidence. Quoting the answer without them is the failure this product exists to prevent, so they travel with it.",
        "operationId": "askSurveyData",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["question"],
                "properties": {
                  "question": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataQuestionAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/executive-summary": {
      "get": {
        "summary": "Get an executive summary",
        "description": "Headline metrics and the findings a reader would lead with.\n\n`keyMetrics.avgCompletionTime` is **absent** rather than zero when nobody has finished the survey: `0s` would claim a measurement that was never taken.\n\n**Computed, not generated.** Every figure and sentence comes from a fixed threshold rule over already-computed analytics. No model is called and no AI action allowance is consumed \u2014 `method: \"computed\"` states this in the response, because the MCP tool this backs sits in a family named for AI and an agent relays whatever provenance it is given.",
        "operationId": "getExecutiveSummary",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Executive summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExecutiveSummary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/report": {
      "get": {
        "summary": "Download the survey report",
        "description": "The survey's report as a **PDF file**, not base64 inside a JSON envelope. A caller writing bytes to a file should not have to decode a string first, and inflating a binary by a third to pass it through JSON is a cost paid on every request.\n\nOmit `sections` for the whole report. An unrecognised section is **refused**, not dropped: a report quietly missing what somebody asked for looks exactly like a report that had nothing to say about it. `X-Report-Sections` on the response states what the file actually contains, since a binary body cannot describe itself.\n\nNo model is involved and no AI allowance is consumed.",
        "operationId": "getSurveyReport",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "sections",
            "in": "query",
            "required": false,
            "description": "Sections to include. Comma-separated or repeated. Defaults to all of them.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "executive_summary",
                  "response_overview",
                  "question_analysis",
                  "quality_metrics",
                  "funnel_analysis"
                ]
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "headers": {
              "Content-Disposition": {
                "description": "Carries the suggested filename.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Report-Sections": {
                "description": "The sections the file actually contains.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/claim-check": {
      "post": {
        "summary": "Check whether the data can carry a claim",
        "description": "Whether the responses collected can support a claim of this kind, and what must be stated alongside it if they can.\n\n**This assesses the sample, not the sentence.** The claim is not read: it is accepted so the assessment can be filed against it and echoed back unchanged. `approved` means the base is fit to support a conclusion \u2014 it is not a finding that the claim is true. The response carries `assesses: \"sample\"` so that cannot be misread.\n\n**Computed, not generated.** Every figure and sentence comes from a fixed threshold rule over already-computed analytics. No model is called and no AI action allowance is consumed \u2014 `method: \"computed\"` states this in the response, because the MCP tool this backs sits in a family named for AI and an agent relays whatever provenance it is given.",
        "operationId": "checkResearchClaim",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["claim"],
                "properties": {
                  "claim": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "description": "The claim being considered. Recorded and echoed back; not analysed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assessment of the sample behind the claim",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClaimAssessment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List webhooks",
        "description": "List webhook subscriptions for your organization. Secrets are masked to secretPreview. Requires the webhooks plan feature (Business or Enterprise) \u2014 returns 403 otherwise.",
        "operationId": "listWebhooks",
        "tags": ["Webhooks"],
        "responses": {
          "200": {
            "description": "List of webhook subscriptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["webhooks"],
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["webhooks:read"]
      },
      "post": {
        "summary": "Create webhook",
        "description": "Create a webhook subscription (the Zapier/Make REST-hook subscribe path). The response is the ONLY place the full signing secret is returned \u2014 store it immediately. URLs must be HTTPS and pass the SSRF guard (no private/internal targets). Requires the webhooks plan feature (Business or Enterprise).",
        "operationId": "createWebhook",
        "tags": ["Webhooks"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created (includes the full signing secret \u2014 shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["webhook"],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/WebhookWithSecret"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["webhooks:write"]
      }
    },
    "/webhooks/{webhookId}": {
      "get": {
        "summary": "Get webhook",
        "description": "Retrieve a single webhook subscription (secret masked). Webhooks belonging to another organization return 404. Requires the webhooks plan feature (Business or Enterprise).",
        "operationId": "getWebhook",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["webhook"],
                  "properties": {
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["webhooks:read"]
      },
      "delete": {
        "summary": "Delete webhook",
        "description": "Delete a webhook subscription (the Zapier/Make REST-hook unsubscribe path). Webhooks belonging to another organization return 404. Requires the webhooks plan feature (Business or Enterprise).",
        "operationId": "deleteWebhook",
        "tags": ["Webhooks"],
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["webhooks:write"]
      }
    },
    "/folders": {
      "get": {
        "summary": "List folders",
        "operationId": "listFolders",
        "tags": ["Organisation"],
        "description": "The organisation's folder tree. Folder permissions are not exposed: they govern who can see what, and granting access is a different act from organising.",
        "responses": {
          "200": {
            "description": "The folder tree",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["folders"],
                  "properties": {
                    "folders": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing the surveys:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a folder",
        "operationId": "createFolder",
        "tags": ["Organisation"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Omit or send null for a top-level folder."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["folder"],
                  "properties": {
                    "folder": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid name, or nesting deeper than three levels",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing the surveys:write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/folders/{folderId}": {
      "parameters": [
        {
          "name": "folderId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "summary": "Rename or move a folder",
        "operationId": "updateFolder",
        "tags": ["Organisation"],
        "description": "Send a name, a parentId, or both. The move is attempted first, so a refused move never leaves a half-applied rename behind.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [],
                  "properties": {
                    "folder": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "moved": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A cycle, the depth cap, or an invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such folder",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a folder",
        "operationId": "deleteFolder",
        "tags": ["Organisation"],
        "description": "Never deletes what is inside it. Surveys and child folders are reparented, and the counts are returned.",
        "responses": {
          "200": {
            "description": "What was reparented",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["deleted"],
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "reparentedFolders": {
                      "type": "integer"
                    },
                    "reparentedSurveys": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing the surveys:write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tags": {
      "get": {
        "summary": "List tags",
        "operationId": "listTags",
        "tags": ["Organisation"],
        "responses": {
          "200": {
            "description": "Tags with the number of surveys each is on",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["tags"],
                  "properties": {
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing the surveys:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/groups": {
      "get": {
        "summary": "List user groups",
        "operationId": "listGroups",
        "tags": ["Organisation"],
        "description": "Read-only. Group membership governs who can see what, so it is managed in the dashboard rather than by an agent.",
        "responses": {
          "200": {
            "description": "Groups with member counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["groups"],
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing the surveys:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/surveys/{surveyId}/tags": {
      "parameters": [
        {
          "name": "surveyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "put": {
        "summary": "Set a survey's tags",
        "operationId": "setSurveyTags",
        "tags": ["Organisation"],
        "description": "Replaces the survey's tags outright \u2014 the body is the complete set afterwards. An empty array clears them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["tagIds"],
                "properties": {
                  "tagIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The survey's tags afterwards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["tags"],
                  "properties": {
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "tagIds must be an array of strings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such survey",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/surveys/{surveyId}/folder": {
      "parameters": [
        {
          "name": "surveyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "put": {
        "summary": "Move a survey to a folder",
        "operationId": "moveSurveyToFolder",
        "tags": ["Organisation"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["folderId"],
                "properties": {
                  "folderId": {
                    "type": "string",
                    "nullable": true,
                    "description": "null moves it out of every folder."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where it now is",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["moved"],
                  "properties": {
                    "moved": {
                      "type": "boolean"
                    },
                    "folderId": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "folderId must be an id or null",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such survey or folder",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/questions/review": {
      "post": {
        "summary": "Review a question's wording",
        "description": "Review one question for bias, leading phrasing, clarity and the usual survey-design faults, returning a score, the issues found, and a suggested revision.\n\nTakes no survey: the question is a string and the organisation comes from the key.\n\n**This one really does call a model**, unlike the other endpoints in this family, so it consumes the organisation's monthly AI action allowance (weight 1). The allowance is checked before the call and recorded only after it succeeds. Organisations supplying their own provider credentials are exempt. Exhausting the allowance returns **402 `quota_exceeded`** \u2014 deliberately not 429, because a monthly allowance does not clear on retry.\n\nIt needs the **`ai:use`** scope rather than a survey scope. Spending a model budget is a different authority from reading a survey, and a key holder should be able to grant one without the other.\n\nWhen no AI provider is configured the review falls back to a local heuristic pass and returns the same shape \u2014 **charging nothing**, since billing for the fallback would charge for the absence of the thing being sold. `method` says which you got: `model` or `computed`.",
        "operationId": "reviewQuestion",
        "tags": ["Questions"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["question"],
                "properties": {
                  "question": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000
                  },
                  "questionType": {
                    "type": "string",
                    "description": "The question type, if known. Defaults to `unknown`, which is reviewed on wording alone."
                  },
                  "context": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Where the question sits, if that changes how it reads."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The review",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionReview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["ai:use"],
        "x-consumes-ai-allowance": {
          "kind": "review",
          "weight": 1
        }
      }
    },
    "/questions/check": {
      "post": {
        "summary": "Check a set of questions",
        "description": "Paste a list of questions, a generic CSV, or a Google Forms export, and get every question reviewed for the usual design faults \u2014 leading phrasing, double-barrelled items, scale asymmetry and so on \u2014 plus an overall score and what the set does well.\n\nTakes no survey: this is for a questionnaire that has not been moved over yet. At most 50 questions are looked at; anything beyond that is ignored rather than refused.\n\n**The cost is not fixed.** Ten questions or fewer are reviewed in one batched model call, and above that it is one call per question \u2014 so a 30-question check consumes 30 units of the AI action allowance, not one. The whole cost is checked before any of the work starts, and a refusal reports `required` alongside `remaining` so a caller can see that a shorter check would still go through.\n\nWhen no AI provider is configured the review falls back to a local pass and **nothing is charged**; `method` says which you got.\n\nNeeds the `ai:use` scope.",
        "operationId": "checkQuestions",
        "tags": ["Questions"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["mode", "content"],
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": ["text", "csv", "google_forms_csv"],
                    "description": "`text` is one question per line (numbering is stripped); `csv` treats the header row as the questions; `google_forms_csv` additionally reports import signals."
                  },
                  "content": {
                    "type": "string",
                    "maxLength": 100000,
                    "description": "The questions, in the shape `mode` describes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The check",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuestionCheck"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["ai:use"],
        "x-consumes-ai-allowance": {
          "kind": "review",
          "weight": 1,
          "units": "1 for ten questions or fewer, otherwise one per question"
        }
      }
    },
    "/contacts": {
      "get": {
        "summary": "List contacts",
        "description": "The organisation's contacts, paginated. `pageSize` above 100 is clamped rather than refused, and the applied value is echoed back.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "listContacts",
        "tags": ["Contacts"],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Substring of email or name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["active", "suppressed"]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contacts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:read"]
      }
    },
    "/contacts/import": {
      "post": {
        "summary": "Import contacts",
        "description": "Bulk create or update contacts, up to 1000 per request.\n\n**`consentAttested` must be `true`** and is checked before a single row is read \u2014 a refusal must not leave personal data sitting on a path that was never allowed to run. The attestation is recorded against every contact with the id of the person who created the key, so the claim has an owner.\n\nA row without an email is refused **by position**, because \"one of your thousand rows is wrong\" is not something a caller can act on.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "importContacts",
        "tags": ["Contacts"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["consentAttested", "rows"],
                "properties": {
                  "consentAttested": {
                    "type": "boolean",
                    "enum": [true],
                    "description": "Confirms every contact has agreed to be contacted."
                  },
                  "rows": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "type": "object",
                      "required": ["email"],
                      "properties": {
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "firstName": {
                          "type": "string"
                        },
                        "lastName": {
                          "type": "string"
                        },
                        "phone": {
                          "type": "string"
                        },
                        "customFields": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What the import did",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attempted": {
                      "type": "integer"
                    },
                    "succeeded": {
                      "type": "integer"
                    },
                    "failed": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:write"]
      }
    },
    "/contact-lists": {
      "get": {
        "summary": "List contact lists",
        "description": "The organisation's contact lists.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "listContactLists",
        "tags": ["Contacts"],
        "responses": {
          "200": {
            "description": "Contact lists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lists": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ContactList"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:read"]
      },
      "post": {
        "summary": "Create a contact list",
        "description": "Create an empty list.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "createContactList",
        "tags": ["Contacts"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:write"]
      }
    },
    "/contact-lists/{listId}/members": {
      "parameters": [
        {
          "name": "listId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "summary": "Add contacts to a list",
        "description": "Add contacts to a list. Both the list and every contact must belong to your organisation.\n\n**All-or-nothing**: a batch containing one contact that is not yours adds nothing. Being told the contacts were added while some were silently dropped leaves you believing the list is complete, and the first you would hear otherwise is a send that missed people.\n\nA list that is not yours, a contact that is not yours and a contact that does not exist all return the **same 404** \u2014 distinguishing them would confirm that a guessed id is real in somebody else's organisation.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "addContactsToList",
        "tags": ["Contacts"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contactIds"],
                "properties": {
                  "contactIds": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many were added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "listId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "added": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:write"]
      }
    },
    "/contacts/suppressions": {
      "get": {
        "summary": "List suppressions",
        "description": "Addresses that will not be emailed, and why.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "listSuppressions",
        "tags": ["Contacts"],
        "responses": {
          "200": {
            "description": "Suppressions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suppressions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "email": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:read"]
      },
      "post": {
        "summary": "Suppress an address",
        "description": "Stop an address receiving mail. The address does not have to be a contact \u2014 somebody who unsubscribed before you imported them is exactly who this is for.\n\nThere is **no API route to un-suppress**. Removing a suppression means mailing a person who asked not to be mailed, which is not something a key should be able to do quietly; the app can, with a human present.\n\nAn unrecognised `reason` is refused rather than defaulted, because filing a hard bounce as an administrative decision changes what the list means to whoever reads it later.\n\nRequires the **emailDistribution** feature (Pro plan or above) \u2014 the same gate the app applies, so a key cannot reach a capability its organisation has not bought. A refusal names the feature, because a caller who cannot tell an entitlement problem from a permission problem will debug the wrong one.",
        "operationId": "suppressContact",
        "tags": ["Contacts"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "unsubscribed",
                      "hard_bounce",
                      "complaint",
                      "manual"
                    ],
                    "default": "manual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Suppressed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["contacts:write"]
      }
    },
    "/surveys/{surveyId}/email-sends": {
      "get": {
        "summary": "List a survey's email sends",
        "description": "Every send made for this survey, and how each is doing.\n\nRequires the **emailDistribution** feature and the `email-sends:read` scope.\n\n**There is no endpoint to create a send.** Putting mail in real people's inboxes is an organisation's decision to delegate, of the same kind made for publishing a survey and uploading reward codes \u2014 and sending email never got the capability switch those two have. The reads are here; the write waits on that decision.",
        "operationId": "listEmailSends",
        "tags": ["Email"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Sends",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "sends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EmailSend"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["email-sends:read"]
      },
      "post": {
        "summary": "Create an email send",
        "description": "Send a survey invitation to a contact list. Requires the emailDistribution feature AND the agentSendEmail capability \u2014 two different questions: whether the organisation has email distribution at all, and whether an agent may use it. agentSendEmail DEFAULTS OFF, uniquely among the agent capabilities, because a send reaches named individuals' inboxes under your own from-address and cannot be recalled. With it off the 403 says the send is ready and a human needs to trigger it from the distribution hub.",
        "operationId": "createEmailSend",
        "tags": ["Email"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEmailSendRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The send was created and its recipients queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateEmailSendResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or the send itself was refused \u2014 an empty list, a list where every contact is suppressed, or the monthly allowance reached. The reason travels in the message."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Missing the emailDistribution feature, or agentSendEmail is off for this organisation. `error.details.feature` says which."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["email-sends:write"]
      }
    },
    "/email-sends/{sendId}": {
      "parameters": [
        {
          "name": "sendId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "Get an email send",
        "description": "One send, with its recipients reported **as counts per status, never as addresses**. \"How did this send go\" does not need everybody's email address to answer, and returning it because it happens to be on the row is how a report becomes an export.\n\nEvery status is present even at zero: a missing key reads as \"we do not know\", while zero is a measurement.\n\nA send belonging to another organisation is **404, not 403** \u2014 the read is already scoped, so there is nothing whose existence could be confirmed.\n\nRequires the **emailDistribution** feature and the `email-sends:read` scope.\n\n**There is no endpoint to create a send.** Putting mail in real people's inboxes is an organisation's decision to delegate, of the same kind made for publishing a survey and uploading reward codes \u2014 and sending email never got the capability switch those two have. The reads are here; the write waits on that decision.",
        "operationId": "getEmailSend",
        "tags": ["Email"],
        "responses": {
          "200": {
            "description": "The send",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "send": {
                      "$ref": "#/components/schemas/EmailSend"
                    },
                    "recipientCounts": {
                      "type": "object",
                      "description": "One entry per status, zero included.",
                      "properties": {
                        "queued": {
                          "type": "integer"
                        },
                        "sent": {
                          "type": "integer"
                        },
                        "delivered": {
                          "type": "integer"
                        },
                        "opened": {
                          "type": "integer"
                        },
                        "responded": {
                          "type": "integer"
                        },
                        "bounced": {
                          "type": "integer"
                        },
                        "complained": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "suppressed": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["email-sends:read"]
      }
    },
    "/email-domains": {
      "get": {
        "summary": "List sending domains",
        "description": "The organisation's sending domains and whether they are verified. `verifiedCount` is returned alongside the list because \"can I send yet\" is the actual question, and answering it should not require knowing that `status === \"verified\"` is the test.\n\nDNS verification records are **not** returned: setting them up ends in somebody's registrar, and an endpoint handing back tokens invites an attempt to automate that.\n\nRequires the **emailDistribution** feature and the `email-sends:read` scope.\n\n**There is no endpoint to create a send.** Putting mail in real people's inboxes is an organisation's decision to delegate, of the same kind made for publishing a survey and uploading reward codes \u2014 and sending email never got the capability switch those two have. The reads are here; the write waits on that decision.",
        "operationId": "listEmailDomains",
        "tags": ["Email"],
        "responses": {
          "200": {
            "description": "Domains",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domains": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "domain": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "lastCheckedAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "verifiedAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "verifiedCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["email-sends:read"]
      }
    },
    "/surveys/{surveyId}/collectors": {
      "get": {
        "summary": "List a survey's collectors",
        "description": "Every collector on the survey, with the sessions each has started and completed.\n\n**Not gated on `namedCollectors`.** Every survey has default collectors whether or not the organisation pays for named ones, so refusing to list them would hide something the customer already has. Creating one is the paid capability.\n\n`token` is the `?c=` value that appears in the distribution URL \u2014 an identifier, not a secret.",
        "operationId": "listCollectors",
        "tags": ["Collectors"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Collectors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "collectors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Collector"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "post": {
        "summary": "Create a collector",
        "description": "Create a named collector. Requires the **namedCollectors** feature.\n\n`type` has **no default**: a collector's type decides how respondents reach it and how their session is attributed, so guessing `link` for a caller who meant `panel` would mislabel every response that arrives through it, permanently.",
        "operationId": "createCollector",
        "tags": ["Collectors"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "type"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "type": {
                    "type": "string",
                    "enum": ["link", "qr", "email", "embed", "panel", "slack"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The collector",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/collectors/{collectorId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        },
        {
          "name": "collectorId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "patch": {
        "summary": "Rename or open/close a collector",
        "description": "Rename a collector, open or close it, or both. Requires the **namedCollectors** feature.\n\n**A default collector can be closed but not renamed.** Its name is the canonical label for a channel, while closing it is a real distribution decision \u2014 respondents arriving at a closed collector see the collector-closed screen.\n\nBoth halves are checked before either is applied. A rename that succeeds followed by a refused status change would leave the caller with half of what they asked for and no way to tell which half.",
        "operationId": "updateCollector",
        "tags": ["Collectors"],
        "requestBody": {
          "required": true,
          "description": "At least one of `name` or `status`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "status": {
                    "type": "string",
                    "enum": ["open", "closed"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The collector",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/variables": {
      "get": {
        "summary": "List a survey's variables",
        "description": "The survey's variable definitions.\n\n**Not gated on `calculatedFields`**: a survey's variables are part of its definition, and hiding them from its owner would misrepresent the instrument. Defining and editing them is the paid capability.\n\nThe compiled expression tree is never returned \u2014 an implementation detail, and a large one.",
        "operationId": "listSurveyVariables",
        "tags": ["Variables"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Variables",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "variables": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SurveyVariable"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "post": {
        "summary": "Define a variable",
        "description": "Define a static or computed variable. Requires the **calculatedFields** feature.\n\nAn expression is validated **on save** \u2014 parse errors, references to questions that are not in this survey, and cycles. A rejection comes back as **400 carrying the validator's own message**, which names the reference or token at fault. That is the part worth keeping, so it is not reworded.",
        "operationId": "createSurveyVariable",
        "tags": ["Variables"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "type"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "type": {
                    "type": "string",
                    "enum": ["static", "computed"]
                  },
                  "expressionSource": {
                    "type": "string",
                    "description": "Required when `type` is `computed`. May reference questions as `@{uuid}` and other variables as `$name`."
                  },
                  "staticValue": {
                    "type": "string",
                    "description": "Required when `type` is `static`."
                  },
                  "formatDecimals": {
                    "type": "integer"
                  },
                  "formatPrefix": {
                    "type": "string"
                  },
                  "formatSuffix": {
                    "type": "string"
                  },
                  "sortOrder": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The variable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SurveyVariable"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/variables/{variableId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        },
        {
          "name": "variableId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "put": {
        "summary": "Replace a variable",
        "description": "Replace the definition wholesale. **PUT rather than PATCH**: the expression is re-validated against the survey on save, and a partial update would have to merge first \u2014 leaving a variable half-described by the caller and half by its previous state, which neither of them can reason about.\n\nAn expression is validated **on save** \u2014 parse errors, references to questions that are not in this survey, and cycles. A rejection comes back as **400 carrying the validator's own message**, which names the reference or token at fault. That is the part worth keeping, so it is not reworded.",
        "operationId": "updateSurveyVariable",
        "tags": ["Variables"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "type"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "type": {
                    "type": "string",
                    "enum": ["static", "computed"]
                  },
                  "expressionSource": {
                    "type": "string",
                    "description": "Required when `type` is `computed`. May reference questions as `@{uuid}` and other variables as `$name`."
                  },
                  "staticValue": {
                    "type": "string",
                    "description": "Required when `type` is `static`."
                  },
                  "formatDecimals": {
                    "type": "integer"
                  },
                  "formatPrefix": {
                    "type": "string"
                  },
                  "formatSuffix": {
                    "type": "string"
                  },
                  "sortOrder": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The variable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SurveyVariable"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      },
      "delete": {
        "summary": "Delete a variable",
        "description": "Remove a variable. **404 when it does not exist**, rather than a 200 reporting a successful no-op \u2014 a caller checking the status code should not be told the delete worked when there was nothing to delete.\n\nA variable that another one's expression references cannot be removed; the 400 says which.",
        "operationId": "deleteSurveyVariable",
        "tags": ["Variables"],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/calibration/priors": {
      "get": {
        "summary": "Get a calibration prior",
        "description": "The aggregated prior for a category, with hierarchical fallback: category + question type + industry, then narrower combinations, then category alone.\n\n**Platform-wide, not organisation-scoped** \u2014 these are aggregates over every calibration the platform has run, which is what makes them useful as priors. Nothing is returned below **50 observations**, so a prior can never be one other customer's experiment wearing a statistic's clothes.\n\n`found: false` is a normal answer for a category nobody has calibrated yet, not a 404. `isStale` is reported rather than filtered on: a stale prior is still the best available answer, and hiding the staleness would let a caller treat it as current.",
        "operationId": "getCalibrationPrior",
        "tags": ["Calibration"],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "questionType",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "industry",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The prior, or found: false",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalibrationPrior"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/calibration/insignificance": {
      "get": {
        "summary": "Predict whether a test will show nothing",
        "description": "Whether a test in this category is likely to produce no measurable difference \u2014 worth asking before spending sample on it.\n\nSubject to the same 50-observation floor. Below it the answer is `isInsignificant: false` with `basedOnN: 0` and `confidence: 0`, which means **we do not know**, not \"go ahead\".",
        "operationId": "predictInsignificance",
        "tags": ["Calibration"],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "questionType",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The prediction",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "isInsignificant": {
                      "type": "boolean"
                    },
                    "confidence": {
                      "type": "number"
                    },
                    "basedOnN": {
                      "type": "integer"
                    },
                    "insignificanceRate": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/calibration/multi-arm": {
      "get": {
        "summary": "Get multi-arm test status",
        "description": "Where each arm stands against the control, by mSPRT with a Bonferroni-corrected threshold \u2014 every extra arm is another chance to see a winner that is not there, so the bar each must clear gets higher.\n\n**A survey nobody has split is answered, not refused.** `comparable: false` with a `notComparableReason` of `fewer_than_two_active_variants` or `no_control_variant`, at 200. \"Is there a winner yet?\" has a true answer for a one-variant survey.\n\n`hasMinimumSample` travels with every arm because mSPRT can converge on too little data, and a converged result on a thin sample is not a result.",
        "operationId": "getMultiArmStatus",
        "tags": ["Calibration"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiArmStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/calibration/fidelity": {
      "get": {
        "summary": "Get per-variant fidelity scores",
        "description": "Fidelity, sample size and completion rate per variant, from the latest metrics row for each.\n\n`avgFidelityScore` is **null** for a variant nobody has been scored on. Null rather than zero: zero is a fidelity score, and a bad one.",
        "operationId": "getFidelityScores",
        "tags": ["Calibration"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Scores",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "variants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "isControl": {
                            "type": "boolean"
                          },
                          "avgFidelityScore": {
                            "type": "number",
                            "nullable": true
                          },
                          "sampleSize": {
                            "type": "integer"
                          },
                          "completionRate": {
                            "type": "number",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/calibration/strategy": {
      "put": {
        "summary": "Set the calibration objective",
        "description": "What \"better\" means for this survey. A preset name resolves to its weights; `custom` takes your own, each in [0, 1] and summing to approximately 1.\n\nThe tolerance is deliberate: a caller writing thirds cannot make three decimals sum to exactly one, and refusing 0.999 would be pedantry about floating point rather than about the objective.\n\nThe response returns the weights **actually stored**, which for a preset are not what the caller sent \u2014 they sent a name.",
        "operationId": "setCalibrationStrategy",
        "tags": ["Calibration"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["strategy"],
                "properties": {
                  "strategy": {
                    "type": "string",
                    "enum": [
                      "completion",
                      "quality",
                      "fidelity",
                      "balanced",
                      "custom"
                    ]
                  },
                  "weights": {
                    "$ref": "#/components/schemas/ObjectiveWeights"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored objective",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "strategy": {
                      "type": "string"
                    },
                    "weights": {
                      "$ref": "#/components/schemas/ObjectiveWeights"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/instruments": {
      "post": {
        "summary": "Add a standardised instrument",
        "description": "Add SUS or UMUX-Lite to a survey as a block of canonical items. Requires the **standardizedInstruments** feature.\n\nThe items are canonical: their wording *is* the instrument, and changing it makes the score incomparable to every published norm. That is enforced by the locked-item rules downstream \u2014 worth knowing when adding one through an agent.",
        "operationId": "addInstrument",
        "tags": ["Instruments"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["instrumentType"],
                "properties": {
                  "instrumentType": {
                    "type": "string",
                    "enum": ["sus", "umux_lite"]
                  },
                  "subject": {
                    "type": "string",
                    "description": "What is being rated, when a survey scores more than one thing."
                  },
                  "randomizeItems": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The instrument and its items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "instrumentId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "instrumentType": {
                      "type": "string"
                    },
                    "subject": {
                      "type": "string",
                      "nullable": true
                    },
                    "questions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string"
                          },
                          "position": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      },
      "get": {
        "summary": "List a survey's instruments",
        "description": "The standardised instruments on a survey. NOT entitlement-gated, unlike adding one: an organisation that drops below the plan must still be able to see what is already in its surveys.",
        "operationId": "listSurveyInstruments",
        "tags": ["Instruments"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "The instruments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["surveyId", "instruments"],
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "instruments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "instrumentType": {
                            "type": "string",
                            "enum": ["sus", "umux_lite"]
                          },
                          "subject": {
                            "type": "string",
                            "nullable": true
                          },
                          "position": {
                            "type": "integer"
                          },
                          "randomizeItems": {
                            "type": "boolean"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      }
    },
    "/surveys/{surveyId}/instruments/{instrumentId}/score": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        },
        {
          "name": "instrumentId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "Get an instrument's score",
        "description": "**Read, never computed here.** The score comes from the polarity-correct scoring library \u2014 SUS reverses its odd-numbered items, and a score computed by anything that does not know that is wrong in a way that looks plausible. No model is involved.\n\n`result` is the library's own shape, unreshaped, because it carries the sample size and grade alongside the number: a score without its n is a number somebody will quote.",
        "operationId": "getInstrumentScore",
        "tags": ["Instruments"],
        "responses": {
          "200": {
            "description": "The score",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "instrumentId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "type": {
                      "type": "string",
                      "enum": ["sus", "umux_lite"]
                    },
                    "subject": {
                      "type": "string",
                      "nullable": true
                    },
                    "result": {
                      "type": "object",
                      "description": "Score, sample size and grade, from the scoring library."
                    },
                    "method": {
                      "type": "string",
                      "enum": ["computed"]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/probes": {
      "get": {
        "summary": "List probe transcripts",
        "description": "The follow-up questions asked of respondents and what they said. Requires the **adaptiveProbes** feature.\n\n**Paginated, unlike the MCP tool.** There is one row per probe per session, each carrying a respondent's free text, so a busy survey has tens of thousands. `total` is returned alongside the page, because otherwise the only signal that there is more is a full page \u2014 which is indistinguishable from a survey with exactly that many probes.",
        "operationId": "listProbeTranscripts",
        "tags": ["Instruments"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of transcripts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "transcripts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "probeId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "sessionId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "parentQuestionId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "depth": {
                            "type": "integer",
                            "description": "How many probes deep this follow-up sits."
                          },
                          "probeText": {
                            "type": "string"
                          },
                          "answer": {},
                          "status": {
                            "type": "string"
                          },
                          "displayedAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "answeredAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/debrief-runs": {
      "post": {
        "summary": "Create a debrief run",
        "description": "Create a cognitive-debriefing run, **in draft**. It does not start: sampling begins from the survey's validity tab, because starting it puts a configuration in front of real respondents and that should not happen without a human having seen it.\n\nThree things are checked and none are ornamental: the `cognitiveDebrief` feature; **edit permission for the key's creator** \u2014 a key acts as the person who made it, and one held by somebody with read access must not start sampling; and a **declared intended meaning** on the question, because the alignment score is computed against it and a run without one produces a number with nothing to compare it to.\n\n`maxTurns` is snapshotted at creation so a config edit mid-run cannot move a live run's budget.",
        "operationId": "createDebriefRun",
        "tags": ["Debriefs"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["questionId", "samplingRate", "targetN"],
                "properties": {
                  "questionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "samplingRate": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 1
                  },
                  "targetN": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10000
                  },
                  "collectorId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Restrict sampling to one collector."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created, in draft",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "questionId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["draft"]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/debrief-runs/{runId}": {
      "parameters": [
        {
          "name": "runId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "Get a debrief run",
        "description": "A run's status, sampling progress and \u2014 once analysed and above the floor \u2014 its alignment metrics and interpretation categories with up to three verbatim excerpts each, highest confidence first.\n\n**The anonymity floor governs this.** Below 5 classified respondents no evidence leaves the database \u2014 no alignment score, no categories, no verbatim excerpts. `anonymityFloor` and `suppressedBelowFloor` are always present, so a caller can tell \"there is nothing here\" from \"we will not tell you\".\n\nA run that is not yours and a run that does not exist are the **same 404**: telling them apart confirms a guessed id is real somewhere.",
        "operationId": "getDebriefRun",
        "tags": ["Debriefs"],
        "responses": {
          "200": {
            "description": "The run",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DebriefRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/debrief-runs/{runId}/transcripts": {
      "parameters": [
        {
          "name": "runId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "List a run's transcripts",
        "description": "Completed conversations, verbatim, cursor-paginated.\n\n**Refused below the anonymity floor, not emptied.** A 403 says why; an empty page would read as \"this run has no conversations\", which is a different and misleading statement, and would invite a caller to retry until the number changed.",
        "operationId": "listDebriefTranscripts",
        "tags": ["Debriefs"],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of transcripts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "transcripts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    },
                    "pageSize": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/rewards": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        }
      ],
      "get": {
        "summary": "Get the reward configuration",
        "description": "The survey's reward configuration and how many codes remain. Requires the **surveyRewards** feature.\n\nThe stock travels with the configuration because \"rewards are on\" and \"there are codes left to give out\" are different facts, and a survey promising a voucher it cannot deliver is worse than one promising nothing.",
        "operationId": "getRewardConfig",
        "tags": ["Rewards"],
        "responses": {
          "200": {
            "description": "Configuration and stock",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "rewardConfig": {
                      "type": "object",
                      "nullable": true
                    },
                    "stock": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "claimed": {
                          "type": "integer"
                        },
                        "remaining": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "put": {
        "summary": "Set the reward configuration",
        "description": "Replace the configuration. Validated against the reward schema, which refuses a non-HTTPS redemption URL and an out-of-range low-stock threshold; a rejection names the field.\n\nThis needs the **surveyRewards** feature but NOT the agent-codes permission \u2014 an organisation may reasonably let an agent configure rewards and not let it upload the secrets.",
        "operationId": "setRewardConfig",
        "tags": ["Rewards"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["config"],
                "properties": {
                  "config": {
                    "type": "object",
                    "description": "A complete reward configuration."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored configuration",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "rewardConfig": {
                      "type": "object"
                    },
                    "stock": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "claimed": {
                          "type": "integer"
                        },
                        "remaining": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/rewards/codes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        }
      ],
      "post": {
        "summary": "Add reward codes",
        "description": "Add voucher codes to the pool.\n\n**This writes secrets that cannot be un-uploaded**, so it needs two things: the `surveyRewards` feature, and the organisation's separate `agentRewardCodes` permission (D-63). The second is checked **before the request body is read**, so a refusal never has voucher codes in memory on a path that was never allowed to run.\n\nCodes are **never echoed back**: a caller who needs to know what they sent already has it, and an endpoint that repeats secrets puts them in one more log. The response reports how many were NEW \u2014 duplicates are ignored, so re-sending a batch after a timeout does not double the pool.",
        "operationId": "addRewardCodes",
        "tags": ["Rewards"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["codes"],
                "properties": {
                  "codes": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10000,
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What landed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "added": {
                      "type": "integer",
                      "description": "How many were new."
                    },
                    "submitted": {
                      "type": "integer"
                    },
                    "stock": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "claimed": {
                          "type": "integer"
                        },
                        "remaining": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:write"]
      }
    },
    "/surveys/{surveyId}/question-validity": {
      "get": {
        "summary": "Get per-question validity",
        "description": "Whether each question has been shown to mean what its author intended, **for its current wording**.\n\nA wording edit reverts the question to `wording_changed`: the evidence was gathered against text that no longer exists, and carrying the old score forward would be the most misleading thing this endpoint could do.\n\nThree states are reported. A run below the anonymity floor reads as `unvalidated` with no run attached \u2014 matching what the results page shows and what `/debrief-runs/{runId}` suppresses for the same run. The floor is re-checked here even though a below-floor run never qualifies, because the failure it guards against is a respondent-level number appearing on a question badge.\n\nRequires the **cognitiveDebrief** feature.",
        "operationId": "getQuestionValidity",
        "tags": ["Debriefs"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Validity per question",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "anonymityFloor": {
                      "type": "integer"
                    },
                    "questions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "questionId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "validated",
                              "wording_changed",
                              "unvalidated"
                            ]
                          },
                          "run": {
                            "type": "object",
                            "nullable": true,
                            "description": "Present only when validated AND above the floor.",
                            "properties": {
                              "runId": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "waveNumber": {
                                "type": "integer"
                              },
                              "alignmentPct": {
                                "type": "number",
                                "description": "0-100."
                              },
                              "pi": {
                                "type": "number",
                                "description": "Misalignment rate, 0-1."
                              },
                              "confirmed": {
                                "type": "boolean",
                                "description": "Every category verdict is author-sourced."
                              },
                              "classifiedN": {
                                "type": "integer"
                              },
                              "collectorId": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "collectorName": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "categories": {
                            "type": "array",
                            "nullable": true,
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "description": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "aligned": {
                                  "type": "boolean"
                                },
                                "alignmentSource": {
                                  "type": "string",
                                  "enum": ["llm", "author"]
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/surveys/{surveyId}/research-review": {
      "parameters": [
        {
          "$ref": "#/components/parameters/surveyId"
        }
      ],
      "get": {
        "summary": "Get the latest research review",
        "description": "The most recent completed review of the survey's design, with its recommendations.\n\n404 when none has been run \u2014 a survey nobody has reviewed has no review, and reporting that as an empty object would read as a review that found nothing.",
        "operationId": "getResearchReview",
        "tags": ["Analytics"],
        "responses": {
          "200": {
            "description": "The review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "overallScore": {
                      "type": "integer"
                    },
                    "methodologyScore": {
                      "type": "integer"
                    },
                    "structureScore": {
                      "type": "integer"
                    },
                    "statisticsScore": {
                      "type": "integer"
                    },
                    "respondentScore": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "string"
                    },
                    "reviewData": {
                      "type": "object",
                      "description": "The full structured review."
                    },
                    "recommendations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "priority": {
                            "type": "string"
                          },
                          "action": {
                            "type": "string"
                          },
                          "rationale": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["surveys:read"]
      },
      "post": {
        "summary": "Run a research review",
        "description": "Review the survey's design \u2014 methodology, structure, statistics and respondent experience \u2014 and store the result.\n\n**This calls a model**, so it consumes the organisation's AI action allowance. Checked before the call and recorded only once it succeeds; organisations on their own provider credentials are exempt. Needs the `ai:use` scope.\n\nA survey with no questions is a **400**: there is nothing to review, and that is the caller's to fix.\n\n**There is no PATCH.** Accepting or dismissing a recommendation is a person reading a suggestion and deciding; adding it here would let a key clear its own review's findings. That stays in the dashboard, at `/api/dashboard/surveys/{id}/research-review`, which is session-authenticated by design.",
        "operationId": "runResearchReview",
        "tags": ["Analytics"],
        "responses": {
          "201": {
            "description": "The review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "overallScore": {
                      "type": "integer"
                    },
                    "methodologyScore": {
                      "type": "integer"
                    },
                    "structureScore": {
                      "type": "integer"
                    },
                    "statisticsScore": {
                      "type": "integer"
                    },
                    "respondentScore": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "string"
                    },
                    "reviewData": {
                      "type": "object",
                      "description": "The full structured review."
                    },
                    "recommendations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "priority": {
                            "type": "string"
                          },
                          "action": {
                            "type": "string"
                          },
                          "rationale": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["ai:use"],
        "x-consumes-ai-allowance": {
          "kind": "review",
          "weight": 1
        }
      }
    },
    "/surveys/{surveyId}/analytics/experiments": {
      "get": {
        "summary": "Compare A/B variants",
        "description": "The two-arm comparison shown on the Experiments tab: per-variant completion, whether the difference is significant, and what to do about it. NOT the same statistic as /calibration/multi-arm, which runs a Bonferroni-corrected mSPRT across challengers for deciding when to stop \u2014 a survey can be 'not yet converged' on one and 'no significant difference' on the other without either being wrong.",
        "operationId": "compareExperimentVariants",
        "tags": ["Analytics"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "The comparison",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExperimentComparison"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["analytics:read"]
      }
    },
    "/organization": {
      "get": {
        "summary": "Get your organization",
        "description": "The organisation this key belongs to. There is no id in the path by design: a key IS an organisation's key, so the only organisation it can read is its own.",
        "operationId": "getOrganization",
        "tags": ["Organization"],
        "responses": {
          "200": {
            "description": "The organisation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/integrations/google-sheets": {
      "get": {
        "summary": "Google Sheets sync status",
        "description": "Whether the organisation's Google account is connected and how each survey's sheet sync is doing. Read only: connecting an account is an OAuth flow that has to happen in a browser, and configuring a sync is a dashboard action. OAuth tokens are never returned.",
        "operationId": "getSheetSyncStatus",
        "tags": ["Integrations"],
        "responses": {
          "200": {
            "description": "The sync status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SheetSyncStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["integrations:read"]
      }
    },
    "/surveys/{surveyId}/questions/review": {
      "post": {
        "summary": "Review a survey's questions",
        "description": "Review every question on a saved survey for bias, clarity and the usual design faults, with a summary across the whole questionnaire. Distinct from POST /questions/review, which takes one question's wording, and POST /questions/check, which takes raw pasted content for a questionnaire that has not been imported. SPENDS the organisation's AI allowance, and the cost is not fixed: ten questions or fewer are one batched model call, above that it is one call per question. The whole price is checked before any of it is spent, and a 402 reports what this review would have cost.",
        "operationId": "reviewSurveyQuestions",
        "tags": ["AI"],
        "parameters": [
          {
            "$ref": "#/components/parameters/surveyId"
          }
        ],
        "responses": {
          "200": {
            "description": "The review. A survey with no questions returns an empty review rather than an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "surveyId",
                    "summary",
                    "questionReviews",
                    "method"
                  ],
                  "properties": {
                    "surveyId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "totalQuestions": {
                          "type": "integer"
                        },
                        "questionsWithIssues": {
                          "type": "integer"
                        },
                        "errorCount": {
                          "type": "integer"
                        },
                        "warningCount": {
                          "type": "integer"
                        },
                        "overallScore": {
                          "type": "number",
                          "description": "0\u2013100 across the survey."
                        },
                        "recommendations": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "questionReviews": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "questionId": {
                            "type": "string"
                          },
                          "questionTitle": {
                            "type": "string"
                          },
                          "overallScore": {
                            "type": "number"
                          },
                          "isAcceptable": {
                            "type": "boolean"
                          },
                          "issues": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "type": {
                                  "type": "string"
                                },
                                "severity": {
                                  "type": "string"
                                },
                                "explanation": {
                                  "type": "string"
                                },
                                "suggestion": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "suggestedRevision": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "method": {
                      "type": "string",
                      "enum": ["model", "computed"],
                      "description": "Whether a provider was consulted or the deterministic local rules ran. A caller comparing two reviews needs to know which produced each."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The survey has more questions than can be reviewed in one request."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Monthly AI action allowance reached. `error.details.required` is what this review would have cost."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": ["questions:read", "ai:use"]
      }
    },
    "/me": {
      "get": {
        "summary": "Who this key is",
        "description": "The calling key's identity, the scopes it carries and the plan its organisation resolves to. Requires no scope of its own, so a key deliberately narrowed to one family can still ask. Never returns the key itself \u2014 only its id and name.",
        "operationId": "getMe",
        "tags": ["Organization"],
        "responses": {
          "200": {
            "description": "The caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key authentication. Create API keys in your organization settings at /organization/api-keys."
      }
    },
    "parameters": {
      "surveyId": {
        "name": "surveyId",
        "in": "path",
        "required": true,
        "description": "Survey ID (UUID)",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "questionId": {
        "name": "questionId",
        "in": "path",
        "required": true,
        "description": "Question ID (UUID)",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "webhookId": {
        "name": "webhookId",
        "in": "path",
        "required": true,
        "description": "Webhook ID (UUID)",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Insufficient permissions or access denied",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "The organisation's monthly AI action allowance is spent. Distinct from 429: retrying will not clear it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Request limit per minute",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Remaining requests in current window",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix timestamp when limit resets",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Survey": {
        "type": "object",
        "required": [
          "id",
          "title",
          "slug",
          "status",
          "questionCount",
          "responseCount",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique survey identifier"
          },
          "title": {
            "type": "string",
            "description": "Survey title"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Survey description"
          },
          "slug": {
            "type": "string",
            "description": "URL-friendly identifier for the survey"
          },
          "status": {
            "type": "string",
            "enum": ["draft", "live", "paused", "closed"],
            "description": "Current survey status"
          },
          "questionCount": {
            "type": "integer",
            "description": "Number of questions in the survey"
          },
          "responseCount": {
            "type": "integer",
            "description": "Number of completed responses"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the survey went live. Null if it never has."
          },
          "closedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the survey closed. Null while it is still collecting."
          }
        }
      },
      "SurveyWithQuestions": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Survey"
          },
          {
            "type": "object",
            "properties": {
              "questions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Question"
                },
                "description": "Questions (included if the API key has questions:read scope)"
              },
              "localization": {
                "$ref": "#/components/schemas/SurveyLocalization"
              },
              "variables": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SurveyVariableDefinition"
                },
                "description": "Calculated-field definitions. Returned only with include=variables."
              },
              "access": {
                "$ref": "#/components/schemas/SurveyAccess"
              }
            }
          }
        ]
      },
      "CreateSurveyRequest": {
        "type": "object",
        "required": ["title"],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Survey title"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "description": "Survey description"
          },
          "welcomeMessage": {
            "type": "string",
            "maxLength": 2000,
            "description": "Message shown at survey start"
          },
          "thankYouMessage": {
            "type": "string",
            "maxLength": 2000,
            "description": "Message shown at survey completion"
          }
        }
      },
      "UpdateSurveyRequest": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Survey title"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "Survey description"
          },
          "welcomeMessage": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "Message shown at survey start"
          },
          "thankYouMessage": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "Message shown at survey completion"
          },
          "status": {
            "type": "string",
            "enum": ["draft", "live", "paused", "closed", "archived"],
            "description": "Target lifecycle status. Transitions are enforced: draft->live, live->paused|closed, paused->live|closed, closed->live|archived, archived->closed. Publishing (->live) additionally requires the agentPublishSurveys capability for API callers."
          }
        }
      },
      "Question": {
        "type": "object",
        "required": ["id", "type", "title", "required", "position", "config"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique question identifier"
          },
          "type": {
            "type": "string",
            "enum": [
              "single_choice",
              "multiple_choice",
              "dropdown",
              "image_choice",
              "image_multiple_choice",
              "icon_choice",
              "forced_choice",
              "likert",
              "nps",
              "stars",
              "slider",
              "constant_sum",
              "ranking",
              "sorting",
              "partial_ranking",
              "matrix_single",
              "matrix_multiple",
              "multiple_number",
              "short_text",
              "long_text",
              "number",
              "email",
              "phone",
              "url",
              "date",
              "time",
              "datetime",
              "date_range",
              "location",
              "address",
              "file_upload",
              "signature",
              "consent",
              "payment",
              "statement",
              "welcome_screen",
              "thank_you_screen"
            ],
            "description": "Question type"
          },
          "title": {
            "type": "string",
            "description": "Question text"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Optional help text or description"
          },
          "required": {
            "type": "boolean",
            "description": "Whether the question requires an answer"
          },
          "position": {
            "type": "integer",
            "description": "Question order (0-indexed)"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Type-specific configuration (options, scale settings, etc.)"
          },
          "debriefConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Cognitive-debriefing configuration, including the author's declared intended meaning. Present only for organisations in the cognitiveDebrief rollout; the key is OMITTED otherwise, never null-filled \u2014 a null would say no intended meaning has been declared. Authoring one requires the same feature and is refused with a 403 without it."
          },
          "media": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Stem media (image, video or GIF) shown with the question. Its own column, not part of `config`. Null when the question has none; the key is always present."
          },
          "probeConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "AI follow-up probe configuration. Its own column, not part of `config`. Null when the question has none."
          }
        }
      },
      "CreateQuestionRequest": {
        "type": "object",
        "required": ["type", "title"],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "single_choice",
              "multiple_choice",
              "dropdown",
              "image_choice",
              "image_multiple_choice",
              "icon_choice",
              "forced_choice",
              "likert",
              "nps",
              "stars",
              "slider",
              "constant_sum",
              "ranking",
              "sorting",
              "partial_ranking",
              "matrix_single",
              "matrix_multiple",
              "multiple_number",
              "short_text",
              "long_text",
              "number",
              "email",
              "phone",
              "url",
              "date",
              "time",
              "datetime",
              "date_range",
              "location",
              "address",
              "file_upload",
              "signature",
              "consent",
              "payment",
              "statement",
              "welcome_screen",
              "thank_you_screen"
            ],
            "description": "Question type"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Question text"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "Optional help text"
          },
          "required": {
            "type": "boolean",
            "default": true,
            "description": "Whether the question requires an answer"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "default": {},
            "description": "Type-specific configuration"
          },
          "position": {
            "type": "integer",
            "minimum": 0,
            "description": "Question order (auto-assigned if not provided)"
          },
          "media": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Stem media (image, video or GIF). Validated against the same schema the app uses; `asset.alt` is required, because a stem image without alt text is inaccessible. Null clears it."
          },
          "probeConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "AI follow-up probe configuration. Null clears it."
          },
          "debriefConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Cognitive-debriefing configuration, including the declared intended meaning. Authoring one requires the cognitiveDebrief feature; without it the request is refused with 403. Null clears it."
          },
          "acknowledgeConsequence": {
            "type": "boolean",
            "description": "Required to change a question on a LIVE survey. Everyone who answers after the change answers a different questionnaire from the people who already have, and the results will not distinguish them. Pausing the survey first is usually the better move."
          }
        }
      },
      "UpdateQuestionRequest": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "single_choice",
              "multiple_choice",
              "dropdown",
              "image_choice",
              "image_multiple_choice",
              "icon_choice",
              "forced_choice",
              "likert",
              "nps",
              "stars",
              "slider",
              "constant_sum",
              "ranking",
              "sorting",
              "partial_ranking",
              "matrix_single",
              "matrix_multiple",
              "multiple_number",
              "short_text",
              "long_text",
              "number",
              "email",
              "phone",
              "url",
              "date",
              "time",
              "datetime",
              "date_range",
              "location",
              "address",
              "file_upload",
              "signature",
              "consent",
              "payment",
              "statement",
              "welcome_screen",
              "thank_you_screen"
            ],
            "description": "Question type"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Question text"
          },
          "description": {
            "type": "string",
            "maxLength": 2000,
            "nullable": true,
            "description": "Optional help text"
          },
          "required": {
            "type": "boolean",
            "description": "Whether the question requires an answer"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Type-specific configuration"
          },
          "position": {
            "type": "integer",
            "minimum": 0,
            "description": "Question order"
          },
          "media": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Stem media (image, video or GIF). Validated against the same schema the app uses; `asset.alt` is required, because a stem image without alt text is inaccessible. Null clears it."
          },
          "probeConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "AI follow-up probe configuration. Null clears it."
          },
          "debriefConfig": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Cognitive-debriefing configuration, including the declared intended meaning. Authoring one requires the cognitiveDebrief feature; without it the request is refused with 403. Null clears it."
          },
          "acknowledgeConsequence": {
            "type": "boolean",
            "description": "Required to change a question on a LIVE survey. Everyone who answers after the change answers a different questionnaire from the people who already have, and the results will not distinguish them. Pausing the survey first is usually the better move."
          }
        }
      },
      "Response": {
        "type": "object",
        "required": ["id", "sessionId", "status", "startedAt", "answers"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Response ID"
          },
          "sessionId": {
            "type": "string",
            "description": "Respondent session identifier"
          },
          "status": {
            "type": "string",
            "enum": ["in_progress", "completed", "abandoned"],
            "description": "Response status"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the response started"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the response was completed"
          },
          "deviceType": {
            "type": "string",
            "nullable": true,
            "description": "Detected device type (mobile, tablet, desktop)"
          },
          "country": {
            "type": "string",
            "nullable": true,
            "description": "Detected country code"
          },
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Answer"
            },
            "description": "Individual question answers"
          }
        }
      },
      "Answer": {
        "type": "object",
        "required": ["questionId", "questionTitle", "value", "answeredAt"],
        "properties": {
          "questionId": {
            "type": "string",
            "format": "uuid",
            "description": "Question ID"
          },
          "questionTitle": {
            "type": "string",
            "description": "Question text for reference"
          },
          "value": {
            "description": "Answer value (type varies by question type)"
          },
          "answeredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the answer was submitted"
          }
        }
      },
      "ExportResponse": {
        "type": "object",
        "required": [
          "survey",
          "questions",
          "responses",
          "exportedAt",
          "totalResponses"
        ],
        "properties": {
          "survey": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                }
              }
            }
          },
          "responses": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "exportedAt": {
            "type": "string",
            "format": "date-time"
          },
          "totalResponses": {
            "type": "integer"
          }
        }
      },
      "Analytics": {
        "type": "object",
        "required": [
          "surveyId",
          "responseStats",
          "timing",
          "questions",
          "daily",
          "generatedAt"
        ],
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "responseStats": {
            "type": "object",
            "required": [
              "total",
              "completed",
              "abandoned",
              "inProgress",
              "completionRate",
              "screenedOut",
              "overQuota",
              "qualityTerminated",
              "totalSessions"
            ],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Qualified sessions: completed + abandoned + in progress. This is the completion-rate denominator."
              },
              "completed": {
                "type": "integer",
                "description": "Completed responses"
              },
              "abandoned": {
                "type": "integer",
                "description": "Abandoned responses"
              },
              "inProgress": {
                "type": "integer",
                "description": "In-progress responses"
              },
              "completionRate": {
                "type": "number",
                "description": "completed / total, as a percentage to one decimal. Disqualified sessions are excluded: a respondent a screener turned away never had the opportunity to complete."
              },
              "screenedOut": {
                "type": "integer",
                "description": "Sessions a screener turned away. Not in the denominator."
              },
              "overQuota": {
                "type": "integer",
                "description": "Sessions rejected because their quota cell was full. Not in the denominator."
              },
              "qualityTerminated": {
                "type": "integer",
                "description": "Sessions ended mid-survey by a quality rule. Not in the denominator."
              },
              "totalSessions": {
                "type": "integer",
                "description": "Every session that arrived, whatever became of it. Always >= total."
              }
            }
          },
          "timing": {
            "type": "object",
            "properties": {
              "averageCompletionSeconds": {
                "type": "integer",
                "nullable": true,
                "description": "Average time to complete in seconds"
              },
              "medianCompletionSeconds": {
                "type": "integer",
                "nullable": true,
                "description": "Median time to complete in seconds"
              }
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "questionId",
                "questionTitle",
                "questionType",
                "responseCount",
                "skipRate"
              ],
              "properties": {
                "questionId": {
                  "type": "string"
                },
                "questionTitle": {
                  "type": "string"
                },
                "questionType": {
                  "type": "string"
                },
                "responseCount": {
                  "type": "integer"
                },
                "skipRate": {
                  "type": "number"
                }
              }
            },
            "description": "Question-level statistics"
          },
          "daily": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "date",
                "responses",
                "completions",
                "completionRate"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "responses": {
                  "type": "integer",
                  "description": "Sessions started that day."
                },
                "completions": {
                  "type": "integer"
                },
                "completionRate": {
                  "type": "number",
                  "description": "completions / responses for that day, as a percentage."
                }
              }
            },
            "description": "Daily counts for the last 30 days, anchored to midnight and zero-filled: a day with no sessions is present with zeros rather than absent."
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "devices": {
            "type": "array",
            "description": "Device-type breakdown. Present only when requested via include=devices.",
            "items": {
              "type": "object",
              "required": ["device", "count", "percentage"],
              "properties": {
                "device": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "percentage": {
                  "type": "number"
                }
              }
            }
          },
          "geography": {
            "type": "array",
            "description": "Country breakdown. Present only when requested via include=geography.",
            "items": {
              "type": "object",
              "required": ["country", "count", "percentage"],
              "properties": {
                "country": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "percentage": {
                  "type": "number"
                }
              }
            }
          },
          "quality": {
            "type": "object",
            "description": "Response-quality summary. Present only when requested via include=quality.",
            "required": [
              "totalResponses",
              "avgQualityScore",
              "excludedCount",
              "flaggedCount",
              "flagBreakdown"
            ],
            "properties": {
              "totalResponses": {
                "type": "integer"
              },
              "avgQualityScore": {
                "type": "number"
              },
              "excludedCount": {
                "type": "integer"
              },
              "flaggedCount": {
                "type": "integer"
              },
              "flagBreakdown": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                },
                "description": "Count per flag type (speeding, straightlining, and so on)."
              }
            }
          },
          "confidence": {
            "type": "object",
            "description": "Whether the sample is large enough to draw conclusions from. Present only when requested via include=confidence, which costs an experiments aggregation.",
            "required": [
              "state",
              "progressPercent",
              "canShowConclusions",
              "message"
            ],
            "properties": {
              "state": {
                "type": "string"
              },
              "progressPercent": {
                "type": "number"
              },
              "canShowConclusions": {
                "type": "boolean"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "QuestionAnalytics": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "questionId": {
            "type": "string",
            "format": "uuid",
            "description": "Present only when the caller narrowed to one question."
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "visibilityRate": {
                  "type": "number",
                  "description": "Share (0-100) of respondents shown this question."
                },
                "shownCount": {
                  "type": "integer",
                  "description": "The analysis base for this question."
                },
                "skippedCount": {
                  "type": "integer",
                  "description": "Respondents the logic never asked."
                },
                "responseCount": {
                  "type": "integer"
                },
                "responseRate": {
                  "type": "number",
                  "description": "Answers as a share of those shown."
                },
                "avgTimeSpent": {
                  "type": "number",
                  "nullable": true
                },
                "distribution": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "value": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer"
                      },
                      "percentage": {
                        "type": "number"
                      }
                    }
                  }
                },
                "numericStats": {
                  "type": "object",
                  "nullable": true,
                  "description": "Present for numeric question types only.",
                  "properties": {
                    "mean": {
                      "type": "number"
                    },
                    "median": {
                      "type": "number"
                    },
                    "stdDev": {
                      "type": "number"
                    },
                    "min": {
                      "type": "number"
                    },
                    "max": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ResponseQuality": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "summary": {
            "type": "object",
            "properties": {
              "totalResponses": {
                "type": "integer"
              },
              "avgQualityScore": {
                "type": "number"
              },
              "excludedCount": {
                "type": "integer"
              },
              "flaggedCount": {
                "type": "integer"
              },
              "flagBreakdown": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "stats": {
            "type": "object",
            "properties": {
              "totalFlagged": {
                "type": "integer"
              },
              "excluded": {
                "type": "integer"
              },
              "highRisk": {
                "type": "integer"
              },
              "byFlagType": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "flaggedSessions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "sessionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "responseCount": {
                  "type": "integer"
                },
                "riskScore": {
                  "type": "number",
                  "nullable": true
                },
                "flags": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string"
                      },
                      "severity": {
                        "type": "string",
                        "enum": ["low", "medium", "high", "critical"]
                      },
                      "details": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "limit": {
            "type": "integer",
            "description": "The limit actually applied, after clamping."
          },
          "filter": {
            "type": "string"
          }
        }
      },
      "FunnelAnalysis": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "funnel": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "shownCount": {
                  "type": "integer"
                },
                "responseCount": {
                  "type": "integer"
                },
                "skippedByLogicCount": {
                  "type": "integer",
                  "description": "Routed past by a logic rule \u2014 not a drop-out."
                },
                "abandonedCount": {
                  "type": "integer"
                },
                "dropoffRate": {
                  "type": "number"
                },
                "retentionRate": {
                  "type": "number"
                }
              }
            }
          },
          "exitPoints": {
            "type": "array",
            "description": "Questions respondents most often abandon the survey on.",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "questionTitle": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "exitCount": {
                  "type": "integer"
                },
                "exitPercentage": {
                  "type": "number"
                },
                "avgTimeBeforeExit": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "SurveyFlow": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "graph": {
            "type": "object",
            "properties": {
              "nodes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "questionId": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "type": {
                      "type": "string",
                      "enum": ["start", "question", "end", "exit"]
                    },
                    "position": {
                      "type": "integer"
                    },
                    "visitCount": {
                      "type": "integer",
                      "description": "Respondents that reached this node."
                    }
                  }
                }
              },
              "links": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "target": {
                      "type": "string"
                    },
                    "volume": {
                      "type": "integer",
                      "description": "Respondents that traversed this transition."
                    },
                    "percentage": {
                      "type": "number",
                      "description": "Share (0-100) of the source node's outgoing traffic."
                    },
                    "isLogicJump": {
                      "type": "boolean",
                      "description": "A skip/jump rule caused the transition."
                    }
                  }
                }
              },
              "totalPaths": {
                "type": "integer"
              },
              "avgPathLength": {
                "type": "number"
              },
              "maxPathLength": {
                "type": "integer"
              },
              "minPathLength": {
                "type": "integer"
              }
            }
          },
          "funnel": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "title": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "shownCount": {
                  "type": "integer"
                },
                "responseCount": {
                  "type": "integer"
                },
                "skippedByLogicCount": {
                  "type": "integer",
                  "description": "Routed past by a logic rule \u2014 not a drop-out."
                },
                "abandonedCount": {
                  "type": "integer"
                },
                "dropoffRate": {
                  "type": "number"
                },
                "retentionRate": {
                  "type": "number"
                },
                "avgTimeSpent": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          },
          "loops": {
            "type": "array",
            "description": "Empty for a survey without loops, never absent.",
            "items": {
              "type": "object",
              "properties": {
                "loopId": {
                  "type": "string",
                  "format": "uuid"
                },
                "loopLabel": {
                  "type": "string"
                },
                "questionIds": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "format": "uuid"
                  }
                },
                "avgIterations": {
                  "type": "number"
                },
                "medianIterations": {
                  "type": "number"
                },
                "maxIterations": {
                  "type": "integer"
                },
                "iterations": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "iteration": {
                        "type": "integer"
                      },
                      "reachedCount": {
                        "type": "integer"
                      },
                      "abandonedCount": {
                        "type": "integer"
                      },
                      "dropoffRate": {
                        "type": "number"
                      }
                    }
                  }
                },
                "perItem": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "loopItemId": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      },
                      "reachedCount": {
                        "type": "integer"
                      },
                      "completedCount": {
                        "type": "integer"
                      },
                      "dropoffRate": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          },
          "deadEnds": {
            "type": "array",
            "description": "Questions respondents get stuck on or leave from.",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "questionTitle": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "visitCount": {
                  "type": "integer"
                },
                "exitCount": {
                  "type": "integer"
                },
                "exitPercentage": {
                  "type": "number"
                },
                "reason": {
                  "type": "string",
                  "enum": ["no_logic", "logic_blocks_all", "high_abandonment"]
                }
              }
            }
          },
          "exitPoints": {
            "type": "array",
            "description": "Questions respondents most often abandon the survey on.",
            "items": {
              "type": "object",
              "properties": {
                "questionId": {
                  "type": "string",
                  "format": "uuid"
                },
                "questionTitle": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "exitCount": {
                  "type": "integer"
                },
                "exitPercentage": {
                  "type": "number"
                },
                "avgTimeBeforeExit": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "CrossTabulation": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "rowDimension": {
            "type": "string"
          },
          "colDimension": {
            "type": "string"
          },
          "rowLabels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "colLabels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cells": {
            "type": "array",
            "description": "Row-major: cells[rowIndex][colIndex].",
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "rowValue": {
                    "type": "string"
                  },
                  "colValue": {
                    "type": "string"
                  },
                  "totalSessions": {
                    "type": "integer"
                  },
                  "completedSessions": {
                    "type": "integer"
                  },
                  "dropoffRate": {
                    "type": "number"
                  },
                  "completionRate": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "totals": {
            "type": "object",
            "properties": {
              "rowTotals": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "colTotals": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "grandTotal": {
                "type": "integer"
              }
            }
          }
        }
      },
      "PretestPrediction": {
        "type": "object",
        "properties": {
          "predictedEffect": {
            "type": "number",
            "description": "Predicted change in completion rate, in percentage points, of B relative to A."
          },
          "confidenceInterval": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 2,
            "maxItems": 2,
            "description": "Lower and upper bound of the predicted effect."
          },
          "recommendation": {
            "type": "string"
          },
          "reasoning": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "question_wording",
              "scale_labeling",
              "question_order",
              "response_options",
              "survey_length",
              "tone_formality",
              "other"
            ]
          },
          "model": {
            "type": "string",
            "description": "The model that produced the prediction."
          },
          "latencyMs": {
            "type": "integer"
          }
        }
      },
      "SurveyInsights": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "insights": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Observations that met a threshold. Empty is a valid answer."
          },
          "recommendations": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Only for problems actually reported above."
          },
          "healthScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "method": {
            "type": "string",
            "enum": ["computed"],
            "description": "How the response was produced. `computed` means threshold rules over analytics, with no model involved."
          }
        }
      },
      "DataQuestionAnswer": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string",
            "description": "Echoed back unchanged."
          },
          "answer": {
            "type": "string",
            "description": "Gains a readiness note when the survey has not reached statistical confidence."
          },
          "confidence": {
            "type": "number",
            "description": "0-1. How much weight the answer's own method carries."
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "limitations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isDefensible": {
            "type": "boolean",
            "description": "Whether the base supports the claim. False is a normal answer, not an error."
          },
          "metricsUsed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "suggestedFollowUp": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "enum": ["computed"]
          }
        }
      },
      "ExecutiveSummary": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "summary": {
            "type": "string"
          },
          "keyMetrics": {
            "type": "object",
            "description": "Headline figures, pre-formatted for display. `avgCompletionTime` is absent when nobody has completed the survey.",
            "additionalProperties": {
              "type": ["string", "number"]
            }
          },
          "topFindings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "method": {
            "type": "string",
            "enum": ["computed"],
            "description": "How the response was produced. `computed` means threshold rules over analytics, with no model involved."
          }
        }
      },
      "ClaimAssessment": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "claim": {
            "type": "string",
            "description": "Echoed back unchanged."
          },
          "status": {
            "type": "string",
            "enum": ["approved", "approved_with_caveats", "blocked"],
            "description": "Whether the SAMPLE can support a claim of this kind. Not a verdict on the claim."
          },
          "sampleAssessment": {
            "type": "object",
            "properties": {
              "totalResponses": {
                "type": "integer"
              },
              "isAdequate": {
                "type": "boolean"
              },
              "recommendation": {
                "type": "string",
                "description": "Present only when the sample is inadequate."
              }
            }
          },
          "caveats": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "biasRisks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Skew is distance from balance in either direction \u2014 an all-desktop sample is named as readily as an all-mobile one."
          },
          "assesses": {
            "type": "string",
            "enum": ["sample"],
            "description": "What the status refers to."
          },
          "method": {
            "type": "string",
            "enum": ["computed"],
            "description": "How the response was produced. `computed` means threshold rules over analytics, with no model involved."
          }
        }
      },
      "WebhookEvent": {
        "type": "string",
        "enum": [
          "response.started",
          "response.completed",
          "response.abandoned",
          "survey.created",
          "survey.published",
          "survey.paused",
          "survey.closed",
          "survey.deleted",
          "calibration.started",
          "calibration.completed",
          "variant.promoted",
          "variant.created",
          "pretest.completed",
          "confidence.state_changed",
          "question.created",
          "question.updated",
          "question.deleted"
        ],
        "description": "Webhook event type"
      },
      "Webhook": {
        "type": "object",
        "required": [
          "id",
          "name",
          "url",
          "events",
          "includeAnswers",
          "isActive",
          "headers",
          "secretPreview",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique webhook identifier"
          },
          "name": {
            "type": "string",
            "description": "Display name"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS delivery URL"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEvent"
            },
            "description": "Subscribed event types"
          },
          "includeAnswers": {
            "type": "boolean",
            "description": "When true, response.completed deliveries are enriched with surveyTitle, answers[] and (if the 256KB budget is exceeded) answersTruncated"
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether the webhook is active"
          },
          "headers": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Custom headers sent with each delivery"
          },
          "secretPreview": {
            "type": "string",
            "description": "Masked signing secret (first 8 characters). The full secret is only returned on creation."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp"
          }
        }
      },
      "WebhookWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "required": ["secret"],
            "properties": {
              "secret": {
                "type": "string",
                "description": "Full signing secret \u2014 returned ONLY in the create response. Used for X-Signature-256 HMAC verification."
              }
            }
          }
        ]
      },
      "CreateWebhookRequest": {
        "type": "object",
        "required": ["name", "url", "events"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Display name"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Delivery URL. Must be HTTPS and pass the SSRF guard (no private/reserved/internal targets)."
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/WebhookEvent"
            },
            "description": "Event types to subscribe to"
          },
          "includeAnswers": {
            "type": "boolean",
            "default": false,
            "description": "Enrich response.completed deliveries with surveyTitle and answers[] (opt-in; moves response PII into webhook bodies)"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Custom headers sent with each delivery"
          }
        }
      },
      "PaginatedResponse": {
        "type": "object",
        "required": ["data", "pagination"],
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "pagination": {
            "type": "object",
            "required": ["total", "page", "pageSize", "totalPages", "hasMore"],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Total number of items"
              },
              "page": {
                "type": "integer",
                "description": "Current page number"
              },
              "pageSize": {
                "type": "integer",
                "description": "Items per page"
              },
              "totalPages": {
                "type": "integer",
                "description": "Total number of pages"
              },
              "hasMore": {
                "type": "boolean",
                "description": "Whether more pages exist"
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Additive cursor bootstrap (currently set by the responses endpoint): opaque cursor of the last item on this page, null on the last page. Pass back verbatim as the cursor query parameter to switch to cursor (keyset) mode. Endpoints without cursor mode omit it."
              }
            }
          }
        }
      },
      "CursorPaginatedResponse": {
        "type": "object",
        "required": ["data", "pagination"],
        "description": "Cursor-mode envelope (returned when the cursor query parameter is used). No total/totalPages \u2014 no COUNT query is executed.",
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "pagination": {
            "type": "object",
            "required": ["limit", "hasMore", "nextCursor"],
            "properties": {
              "limit": {
                "type": "integer",
                "description": "Batch size used for this page (pageSize)"
              },
              "hasMore": {
                "type": "boolean",
                "description": "Whether more items exist after this page"
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Opaque cursor for the next page; null on the last page. Pass back verbatim as the cursor query parameter."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "rate_limited",
                  "invalid_request",
                  "internal_error",
                  "invalid_scope",
                  "expired_key",
                  "inactive_key"
                ],
                "description": "Error code"
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message"
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Additional error details (validation errors, etc.)"
              }
            }
          }
        }
      },
      "QuestionReview": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "description": "Echoed back unchanged."
          },
          "overallScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "isAcceptable": {
            "type": "boolean"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "e.g. leading, double-barrelled, jargon."
                },
                "severity": {
                  "type": "string",
                  "enum": ["error", "warning"]
                },
                "explanation": {
                  "type": "string"
                },
                "suggestion": {
                  "type": "string"
                }
              }
            }
          },
          "suggestedRevision": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "enum": ["model", "computed"],
            "description": "`model` when a provider was called and the allowance consumed; `computed` when the local fallback ran and nothing was charged."
          }
        }
      },
      "QuestionCheck": {
        "type": "object",
        "properties": {
          "overallScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "questionCount": {
            "type": "integer",
            "description": "How many questions were actually checked, after the cap."
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "text": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "issues": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "questionIndex": {
                        "type": "integer"
                      },
                      "questionText": {
                        "type": "string"
                      },
                      "severity": {
                        "type": "string",
                        "enum": ["high", "medium", "low"]
                      },
                      "type": {
                        "type": "string"
                      },
                      "message": {
                        "type": "string"
                      },
                      "suggestion": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "questionIndex": {
                  "type": "integer"
                },
                "questionText": {
                  "type": "string"
                },
                "severity": {
                  "type": "string",
                  "enum": ["high", "medium", "low"]
                },
                "type": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "suggestion": {
                  "type": "string"
                }
              }
            },
            "description": "Every issue found, flattened across the set."
          },
          "strengths": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "importSignals": {
            "type": "object",
            "description": "Google Forms mode only.",
            "properties": {
              "typeConfidence": {
                "type": "number"
              },
              "piiDetected": {
                "type": "boolean"
              },
              "ambiguities": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "method": {
            "type": "string",
            "enum": ["model", "computed"],
            "description": "`model` when a provider was called and the allowance consumed; `computed` when the local fallback ran and nothing was charged."
          }
        }
      },
      "ContactPage": {
        "type": "object",
        "properties": {
          "contacts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "email": {
                  "type": "string"
                },
                "firstName": {
                  "type": "string",
                  "nullable": true
                },
                "lastName": {
                  "type": "string",
                  "nullable": true
                },
                "status": {
                  "type": "string",
                  "enum": ["active", "suppressed"]
                },
                "source": {
                  "type": "string"
                },
                "consentStatus": {
                  "type": "string"
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer",
            "description": "The value actually applied, after clamping."
          }
        }
      },
      "ContactList": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "contactCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailSend": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "totalRecipients": {
            "type": "integer"
          },
          "sentCount": {
            "type": "integer"
          },
          "respondedCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Collector": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["link", "qr", "email", "embed", "panel", "slack"]
          },
          "status": {
            "type": "string",
            "enum": ["open", "closed"]
          },
          "isDefault": {
            "type": "boolean",
            "description": "Structural: can be closed, cannot be renamed."
          },
          "token": {
            "type": "string",
            "description": "The `?c=` value in the distribution URL."
          },
          "startedCount": {
            "type": "integer"
          },
          "completedCount": {
            "type": "integer"
          }
        }
      },
      "SurveyVariable": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["static", "computed"]
          },
          "expressionSource": {
            "type": "string",
            "nullable": true,
            "description": "Null for a static variable."
          },
          "staticValue": {
            "type": "string",
            "nullable": true,
            "description": "Null for a computed variable."
          },
          "formatDecimals": {
            "type": "integer",
            "nullable": true
          },
          "formatPrefix": {
            "type": "string",
            "nullable": true
          },
          "formatSuffix": {
            "type": "string",
            "nullable": true
          },
          "sortOrder": {
            "type": "integer"
          }
        }
      },
      "CalibrationPrior": {
        "type": "object",
        "properties": {
          "found": {
            "type": "boolean"
          },
          "prior": {
            "type": "object",
            "nullable": true,
            "properties": {
              "alpha": {
                "type": "number"
              },
              "beta": {
                "type": "number"
              },
              "observationCount": {
                "type": "integer",
                "minimum": 50
              },
              "category": {
                "type": "string"
              },
              "questionType": {
                "type": "string",
                "nullable": true
              },
              "industry": {
                "type": "string",
                "nullable": true
              },
              "isStale": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "ObjectiveWeights": {
        "type": "object",
        "required": ["completion", "quality", "fidelity"],
        "properties": {
          "completion": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "quality": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "fidelity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "MultiArmStatus": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "variantCount": {
            "type": "integer"
          },
          "comparable": {
            "type": "boolean"
          },
          "notComparableReason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "fewer_than_two_active_variants",
              "no_control_variant",
              null
            ]
          },
          "controlVariant": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "sampleSize": {
                "type": "integer"
              },
              "completionRate": {
                "type": "number"
              }
            }
          },
          "challengers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                },
                "sampleSize": {
                  "type": "integer"
                },
                "completionRate": {
                  "type": "number"
                },
                "msprt": {
                  "type": "object",
                  "properties": {
                    "converged": {
                      "type": "boolean"
                    },
                    "lambdaStat": {
                      "type": "number"
                    },
                    "threshold": {
                      "type": "number"
                    },
                    "winner": {
                      "type": "string",
                      "nullable": true,
                      "enum": ["A", "B", null]
                    },
                    "delta": {
                      "type": "number"
                    },
                    "progressPercent": {
                      "type": "number"
                    }
                  }
                },
                "hasMinimumSample": {
                  "type": "boolean",
                  "description": "False means a converged result is not yet a result."
                }
              }
            }
          },
          "bonferroniAlpha": {
            "type": "number"
          }
        }
      },
      "DebriefRun": {
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "format": "uuid"
          },
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "questionId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "waveNumber": {
            "type": "integer"
          },
          "samplingRate": {
            "type": "number"
          },
          "targetN": {
            "type": "integer"
          },
          "maxTurns": {
            "type": "integer"
          },
          "promptVersion": {
            "type": "string"
          },
          "questionVersionHash": {
            "type": "string",
            "description": "A wording edit changes this, which is what makes an earlier wave incomparable."
          },
          "collectorId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "progress": {
            "type": "object",
            "properties": {
              "sampledCount": {
                "type": "integer"
              },
              "conversationCount": {
                "type": "integer"
              },
              "completedCount": {
                "type": "integer"
              },
              "turnsUsed": {
                "type": "integer"
              }
            }
          },
          "analyzed": {
            "type": "boolean"
          },
          "metrics": {
            "type": "object",
            "nullable": true,
            "description": "Null before analysis AND below the floor.",
            "properties": {
              "alignmentScore": {
                "type": "number"
              },
              "misalignmentRate": {
                "type": "number"
              },
              "entropyBits": {
                "type": "number"
              },
              "classifiedN": {
                "type": "integer"
              }
            }
          },
          "confirmed": {
            "type": "boolean"
          },
          "categories": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                },
                "description": {
                  "type": "string",
                  "nullable": true
                },
                "aligned": {
                  "type": "boolean",
                  "nullable": true
                },
                "alignmentSource": {
                  "type": "string",
                  "nullable": true
                },
                "alignmentRationale": {
                  "type": "string",
                  "nullable": true
                },
                "share": {
                  "type": "number"
                },
                "count": {
                  "type": "integer"
                },
                "evidence": {
                  "type": "array",
                  "maxItems": 3,
                  "items": {
                    "type": "object",
                    "properties": {
                      "conversationId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "excerpt": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "anonymityFloor": {
            "type": "integer",
            "description": "Classified respondents required before any evidence is returned."
          },
          "suppressedBelowFloor": {
            "type": "boolean"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "SurveyLocalization": {
        "type": "object",
        "description": "Language settings and per-language translation status. Returned only with include=localization.",
        "required": [
          "defaultLanguage",
          "enabledLanguages",
          "autoDetectLanguage",
          "showLanguageSelector",
          "baseLanguage",
          "translations"
        ],
        "properties": {
          "defaultLanguage": {
            "type": "string"
          },
          "enabledLanguages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "autoDetectLanguage": {
            "type": "boolean"
          },
          "showLanguageSelector": {
            "type": "boolean"
          },
          "baseLanguage": {
            "type": "string",
            "description": "The organisation's default, which a new survey inherits."
          },
          "translations": {
            "type": "array",
            "description": "One entry per enabled language. Empty for a monolingual survey.",
            "items": {
              "type": "object",
              "required": [
                "languageCode",
                "exists",
                "isReviewed",
                "isAiGenerated"
              ],
              "properties": {
                "languageCode": {
                  "type": "string"
                },
                "exists": {
                  "type": "boolean"
                },
                "isReviewed": {
                  "type": "boolean"
                },
                "isAiGenerated": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "SurveyVariableDefinition": {
        "type": "object",
        "description": "A calculated field's definition. The compiled expression is an implementation detail and is not returned.",
        "required": ["id", "name", "type", "sortOrder"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": ["static", "computed"]
          },
          "expressionSource": {
            "type": "string",
            "nullable": true
          },
          "staticValue": {
            "type": "string",
            "nullable": true
          },
          "formatDecimals": {
            "type": "integer",
            "nullable": true
          },
          "formatPrefix": {
            "type": "string",
            "nullable": true
          },
          "formatSuffix": {
            "type": "string",
            "nullable": true
          },
          "sortOrder": {
            "type": "integer"
          }
        }
      },
      "SurveyAccess": {
        "type": "object",
        "description": "Respondent access controls. Returned only with include=access. The stored password hash is never returned in any form.",
        "required": ["passwordProtected", "responsePolicy"],
        "properties": {
          "passwordProtected": {
            "type": "boolean",
            "description": "Whether a password is set. Derived; the hash is not exposed."
          },
          "responsePolicy": {
            "type": "string",
            "enum": ["one_per_device", "multiple", "one_strict"]
          }
        }
      },
      "CreateEmailSendRequest": {
        "type": "object",
        "required": ["contactListId", "subject", "bodyTemplate"],
        "properties": {
          "contactListId": {
            "type": "string",
            "format": "uuid",
            "description": "A contact list in your organisation. A list belonging to another organisation reads as not found."
          },
          "subject": {
            "type": "string",
            "description": "The email subject line."
          },
          "bodyTemplate": {
            "type": "string",
            "description": "The email body. Supports {{name}} and {{survey_link}} placeholders."
          },
          "reminders": {
            "type": "object",
            "nullable": true,
            "description": "Optional reminder cadence. Omit for no reminders.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Defaults to true when the object is present. False is an explicit 'no reminders'."
              },
              "afterDays": {
                "type": "integer",
                "minimum": 1,
                "description": "Days to wait before the first reminder. Defaults to 3."
              },
              "maxReminders": {
                "type": "integer",
                "minimum": 1,
                "maximum": 3,
                "description": "How many reminders at most. Defaults to 1."
              },
              "skipOpened": {
                "type": "boolean",
                "description": "Skip recipients who already opened the invitation."
              }
            }
          }
        }
      },
      "CreateEmailSendResponse": {
        "type": "object",
        "required": ["sendId", "surveyId", "queued", "suppressed"],
        "properties": {
          "sendId": {
            "type": "string",
            "format": "uuid"
          },
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "queued": {
            "type": "integer",
            "description": "Recipients the send will go to."
          },
          "suppressed": {
            "type": "integer",
            "description": "Recipients skipped because they have unsubscribed or previously bounced. Always reported: 'queued' alone hides them."
          }
        }
      },
      "ExperimentComparison": {
        "type": "object",
        "required": [
          "surveyId",
          "variants",
          "significance",
          "winner",
          "recommendation"
        ],
        "properties": {
          "surveyId": {
            "type": "string",
            "format": "uuid"
          },
          "variants": {
            "type": "array",
            "description": "Empty when the survey is not running an A/B test.",
            "items": {
              "type": "object",
              "required": ["name", "isControl", "responses", "completionRate"],
              "properties": {
                "name": {
                  "type": "string"
                },
                "isControl": {
                  "type": "boolean"
                },
                "responses": {
                  "type": "integer"
                },
                "completionRate": {
                  "type": "number"
                }
              }
            }
          },
          "significance": {
            "type": "object",
            "required": ["isSignificant", "pValue", "confidenceLevel"],
            "properties": {
              "isSignificant": {
                "type": "boolean"
              },
              "pValue": {
                "type": "number",
                "nullable": true,
                "description": "Null when no p-value was produced. Never zero-filled: zero would read as p < 0.001."
              },
              "confidenceLevel": {
                "type": "number",
                "description": "95 when significant, 0 when not \u2014 the level the verdict was taken at."
              }
            }
          },
          "winner": {
            "type": "string",
            "nullable": true,
            "description": "The best-performing variant's name, only when the result is significant."
          },
          "recommendation": {
            "type": "string",
            "description": "What to do next. 'Continue collecting data' and 'consider larger changes' are opposite instructions, and which one applies is decided by sample size rather than by the p-value."
          }
        }
      },
      "Organization": {
        "type": "object",
        "required": ["id", "name", "plan", "baseLanguage", "createdAt"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "description": "The plan the entitlement layer resolves, taken from the subscription rather than the organisations column \u2014 the two can disagree, and only the first governs what you may do."
          },
          "baseLanguage": {
            "type": "string",
            "description": "The default a new survey inherits."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SheetSyncStatus": {
        "type": "object",
        "required": ["connection", "syncs"],
        "properties": {
          "connection": {
            "type": "object",
            "required": ["status", "googleEmail"],
            "properties": {
              "status": {
                "type": "string",
                "enum": ["connected", "needs_reauth", "error", "not_connected"],
                "description": "`not_connected` is a state, not an error: an organisation that has not linked an account yet is in an ordinary position."
              },
              "googleEmail": {
                "type": "string",
                "nullable": true
              },
              "lastError": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "syncs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "surveyId": {
                  "type": "string",
                  "format": "uuid"
                },
                "spreadsheetId": {
                  "type": "string"
                },
                "spreadsheetName": {
                  "type": "string",
                  "nullable": true
                },
                "sheetTab": {
                  "type": "string",
                  "nullable": true
                },
                "syncEnabled": {
                  "type": "boolean"
                },
                "lastSyncedAt": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "backfillDone": {
                  "type": "boolean",
                  "description": "Whether the initial historical fill has finished."
                },
                "lastError": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "apiKey": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "lastUsedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "plan": {
                "type": "string",
                "description": "The plan the entitlement layer resolves."
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this key may do \u2014 the same list the endpoints enforce."
          },
          "capabilities": {
            "type": "object",
            "description": "Resolved server-side by the same `can()` the endpoints enforce \u2014 entitlement AND rollout flag AND any organisation override. An organisation can change the agent capabilities for itself, so do not cache these across sessions.",
            "properties": {
              "mcpAccess": {
                "type": "boolean",
                "description": "Whether MCP is available to this plan at all."
              },
              "agentPublishSurveys": {
                "type": "boolean"
              },
              "agentRewardCodes": {
                "type": "boolean"
              },
              "agentSendEmail": {
                "type": "boolean",
                "description": "Off unless the organisation has turned it on (D-74)."
              }
            }
          }
        }
      }
    }
  }
}
