openapi: 3.0.3
info:
  title: Ditto External API
  version: 1.0.0
  description: 'Production-ready endpoints for recruiting research groups, creating studies,

    asking questions, completing workflows, and sharing results.


    **New to Ditto?**

    - [Quick Start: Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

    - [API Workflow Guide Hub](https://askditto.io/claude-code-guide)

    - [Why Ditto for AI Agents](https://askditto.io/claude-code/why-ditto)


    **Use-case guides:** [Customer Segmentation](https://askditto.io/claude-code-guide/customer-segmentation-guide) ·

    [Competitive Intelligence](https://askditto.io/claude-code-guide/competitive-intelligence-guide) ·

    [Pricing Research](https://askditto.io/claude-code-guide/pricing-research-guide) ·

    [Positioning Validation](https://askditto.io/claude-code-guide/validate-product-positioning-guide) ·

    [Product Messaging](https://askditto.io/claude-code-guide/test-product-messaging-guide) ·

    [Sales Enablement](https://askditto.io/claude-code-guide/sales-enablement-guide) ·

    [Voice of Customer](https://askditto.io/claude-code-guide/voice-of-customer-guide) ·

    [GTM Strategy](https://askditto.io/claude-code-guide/gtm-strategy-validation-guide) ·

    [Product Launch](https://askditto.io/claude-code-guide/product-launch-research-guide) ·

    [Content Marketing](https://askditto.io/claude-code-guide/content-marketing-engine-guide) ·

    [Question Design](https://askditto.io/claude-code-guide/question-design-playbook) ·

    [Recruitment Strategies](https://askditto.io/claude-code-guide/recruitment-strategies).


    **Authentication:** All endpoints require a Bearer token.

    Get your API key from [Settings > API Keys](https://app.askditto.io/settings/api-keys)

    in the Ditto dashboard, or use the CLI auth endpoints for programmatic access.

    Include it in every request: `Authorization: Bearer rk_live_your_key_here`.


    **ID formats:** Wherever `{group_id}` or `{study_id}` appears in a path, you can pass

    either the numeric ID (for example `124`) or the UUID (for example `rg_3b9f2c0a`

    / `rs_aa1d9b2f`) unless the endpoint explicitly says UUID-only. When creating studies,

    use `research_group_uuid` from the recruit response.

    '
servers:
- url: https://app.askditto.io
security:
- bearerAuth: []
tags:
- name: ResearchGroups
  description: 'Create, recruit, and manage research groups. Groups are collections of AI personas

    that participate in studies. Most workflows start here.


    Quick path: use `POST /v1/research-groups/recruit` with filters, or

    `POST /v1/research-group-requests` for AI-assisted natural-language recruitment.

    '
- name: ResearchStudies
  description: 'Create studies, ask structured questions, run completion analysis, and share results.

    Studies require an existing research group.

    '
- name: StudyQuestions
  description: 'Ask questions within a study context. Questions are broadcast to all study participants

    and answers arrive asynchronously (poll with Jobs).

    '
- name: Jobs
  description: 'Poll asynchronous job status. Questions, completion workflows, and request-driven flows

    return job IDs. Poll until `status: "finished"`.

    '
- name: ResearchAgents
  description: 'Search and find individual AI personas. Use `GET /v1/agents/search` to browse the pool

    and `GET/POST /v1/agents/find` to match one persona.

    '
- name: GroupQuestions
  description: 'Ask a direct one-off question to an entire research group without creating a full study.

    '
- name: AgentQuestions
  description: 'Ask a direct one-off question to a single research agent without creating a study.

    '
- name: MediaAssets
  description: 'Upload images/PDFs for question attachments. Upload first, then pass `media_asset_id`

    in question requests.

    '
- name: Filters
  description: 'Discover country-scoped categorical values and counts before recruiting/searching.

    Use `GET /v1/filters?country=USA` to avoid filter-value guesswork.

    '
- name: FreeTier
  description: 'Ask a question to the shared free-tier group without full workflow setup.

    '
- name: Zeitgeist
  description: 'Create and manage recurring surveys that run on a schedule and collect periodic snapshots.

    '
- name: CLIAuth
  description: 'OAuth flow for CLI tools and programmatic authentication bootstrap.

    '
- name: Questions
  description: 'Generic question-broadcast tagging used by select endpoints.

    '
paths:
  /cli/auth/start:
    post:
      summary: Start CLI OAuth flow
      description: 'Initializes a CLI OAuth session and returns an authorization URL.

        The redirect URI must be a loopback address (localhost or 127.0.0.1).

        '
      tags:
      - CLIAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CliAuthStartRequest'
      responses:
        '200':
          description: CLI auth session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CliAuthStartResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /cli/auth/complete:
    post:
      summary: Complete CLI OAuth flow
      description: 'Exchanges the authorization code for user info and returns a free-tier API key,

        or a login URL if the user belongs to a paid organization.

        '
      tags:
      - CLIAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CliAuthCompleteRequest'
      responses:
        '200':
          description: CLI auth completed
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/CliAuthCompleteResponse'
                - $ref: '#/components/schemas/CliAuthLoginRequiredResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/free/questions:
    post:
      summary: Ask a question to the free-tier group
      description: 'Sends a single question to the shared free-tier research group.

        '
      tags:
      - FreeTier
      - Questions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FreeTierQuestionCreateRequest'
      responses:
        '202':
          description: Free-tier question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FreeTierQuestionBroadcastResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/media-assets:
    post:
      summary: Upload a media asset for question attachments
      description: 'Uploads one image/PDF attachment and returns a reusable `media_asset.id`.

        You can then pass that value as `media_asset_id` in question attachments for:

        - `POST /v1/research-agents/{agent_id}/questions`

        - `POST /v1/research-groups/{group_id}/questions`

        - `POST /v1/research-studies/{study_id}/questions`

        Requires API key scope `studies:write`.


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [Content Marketing Engine](https://askditto.io/claude-code-guide/content-marketing-engine-guide)

        '
      tags:
      - MediaAssets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaAssetUploadRequest'
            example:
              filename: concept.pdf
              mime: application/pdf
              file_data: JVBERi0xLjQKJcTl8uXrp...
              encoding: base64
          multipart/form-data:
            schema:
              type: object
              required:
              - file
              properties:
                file:
                  type: string
                  format: binary
                  description: 'Binary file upload. Allowed: images (PNG/JPEG/GIF/WEBP) and PDFs.'
      responses:
        '201':
          description: Media asset uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaAssetUploadResponse'
              example:
                media_asset:
                  id: ma_9f0a1c
                  filename: concept.pdf
                  mime: application/pdf
                  size_bytes: 482191
                  width: null
                  height: null
                  created_at: '2026-03-03T12:00:00Z'
                  url: https://app.askditto.io/api/media-assets/ma_9f0a1c
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups:
    post:
      summary: Create a research group from explicit agent IDs
      description: 'Creates a research group using the provided agent IDs (manual composition mode).

        Filter-based recruitment remains available at `POST /v1/research-groups/recruit`.

        '
      tags:
      - ResearchGroups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateResearchGroupFromAgentsRequest'
      responses:
        '201':
          description: Research group created from explicit agent IDs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateResearchGroupFromAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
    get:
      summary: List research groups
      description: 'Returns research groups for the organization tied to the API key.

        Results include group metadata and current `agent_count`.

        Paginated via `limit` and `offset`.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: query
        name: limit
        schema:
          type: integer
          minimum: 1
          maximum: 500
      - in: query
        name: offset
        schema:
          type: integer
          minimum: 0
      responses:
        '200':
          description: Research groups list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups/recruit:
    post:
      summary: Recruit a research group via filters
      description: 'Recruits a research group using the filter DSL and optional target size controls.

        Use `GET /v1/filters?country=USA` to discover available categorical values and counts

        before recruiting.


        Note: top-level `description` sets the group''s description (for your reference).

        To filter agents by profile description, use nested filters

        (`"filters": { "description": { "contains": "grocery shopping" } }`)

        or the shorthand `description_contains` key.


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Customer Segmentation](https://askditto.io/claude-code-guide/customer-segmentation-guide)

        - [Sales Enablement](https://askditto.io/claude-code-guide/sales-enablement-guide)

        - [Voice of Customer](https://askditto.io/claude-code-guide/voice-of-customer-guide)

        - [GTM Strategy Validation](https://askditto.io/claude-code-guide/gtm-strategy-validation-guide)

        - [Product Launch Research](https://askditto.io/claude-code-guide/product-launch-research-guide)

        - [Recruitment Strategies](https://askditto.io/claude-code-guide/recruitment-strategies)

        '
      x-workflow-step: 1
      x-codeSamples:
      - lang: cURL
        label: Recruit Group
        source: "curl -X POST \"$BASE_URL/v1/research-groups/recruit\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H\
          \ \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Midwest Parents\",\n    \"group_size\": 10,\n\
          \    \"filters\": {\n      \"match\": \"all\",\n      \"filters\": {\n        \"country\": { \"any\": [\"USA\"]\
          \ }\n      }\n    }\n  }'\n"
      tags:
      - ResearchGroups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecruitResearchGroupRequest'
            example:
              name: Midwest Parents
              description: People who cook dinner at home and manage grocery shopping.
              filters:
                match: all
                filters:
                  country:
                    any:
                    - USA
                  state:
                    any:
                    - IL
                    - IN
                    - MI
                    - OH
                  age:
                    min: 30
                    max: 45
                  is_parent: true
              group_size: 10
              sample_method: random
              dedupe: true
      responses:
        '201':
          description: ResearchGroup created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecruitResearchGroupResponse'
              example:
                group:
                  id: 124
                  uuid: rg_3b9f2c0a
                  name: Midwest Parents
                  description: People who cook dinner at home and manage grocery shopping.
                  agent_count: 10
                filters:
                  match: all
                  filters:
                    country:
                      any:
                      - USA
                    state:
                      any:
                      - IL
                      - IN
                      - MI
                      - OH
                    age:
                      min: 30
                      max: 45
                    is_parent: true
                recruited_agent_ids:
                - 9012
                - 9013
                recruited_agents:
                - id: 9012
                  url: https://app.askditto.io/organization/org_123/agent/9012
                - id: 9013
                  url: https://app.askditto.io/organization/org_123/agent/9013
        '400':
          description: 'Invalid recruitment payload or filters.

            Recovery:

            - Use `group_size` (not `size` or `sample_size`).

            - Validate filter keys/values against `docs/api/guides/filters.md`.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalid_filters:
                  summary: Unsupported or malformed filters
                  value:
                    error:
                      code: invalid_request
                      message: Invalid filters
                      details:
                        filters:
                          ethnicity:
                          - unsupported value; use an exact documented value (for example "Hispanic or Latino")
                deprecated_size_alias:
                  summary: Deprecated size alias used
                  value:
                    error:
                      code: invalid_request
                      message: size is not supported; use group_size
        '422':
          description: 'Optional strict filter-value validation failure (`API_V1_STRICT_INVALID_FILTERS=1`).

            Returned when categorical values are well-formed but unsupported in current data.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: Invalid filter value(s)
                  details:
                    filters:
                    - field: ethnicity
                      value: hispanic
                      message: unsupported value for ethnicity
                      suggestions:
                      - Hispanic or Latino
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`studies:write`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Conflict, usually duplicate research group name in the same organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: conflict
                  message: Research group name already exists
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/interview:
    post:
      summary: Interview and recruit a research group
      description: 'Queues an interview-driven recruitment flow from a natural-language objective.

        This endpoint is asynchronous and returns a job ID for status polling.

        '
      tags:
      - ResearchGroups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InterviewResearchGroupRequest'
      responses:
        '202':
          description: Interview-driven recruitment queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewResearchGroupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-group-requests:
    post:
      summary: Create or resume a natural-language research group request
      description: 'Accepts a freeform audience description and uses LLM-assisted parsing to build

        a recruitment plan, including clarification and review loops. The service applies

        guardrails based on Ditto recruitment strategies (country whitelists, state code

        normalization, unsupported filter rejection, and over-recruit + curate defaults).

        The service also performs automated screening and pruning using an LLM-driven

        screening question, looping up to 5 times to meet the requested criteria.

        '
      tags:
      - ResearchGroups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
              - $ref: '#/components/schemas/ResearchGroupRequestCreateRequest'
              - $ref: '#/components/schemas/ResearchGroupRequestResumeRequest'
      responses:
        '201':
          description: Research group request completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupRequestCompletedResponse'
        '202':
          description: Request queued for async processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupRequestQueuedResponse'
        '400':
          description: Clarification required before recruitment can proceed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupRequestClarificationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Review required before proceeding with further recruitment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupRequestReviewResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-group-requests/{request_id}:
    get:
      summary: Get status for a natural-language research group request
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: request_id
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Research group request status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupRequestStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/{group_uuid}/append:
    post:
      summary: Add agents to an existing research group
      description: 'Appends additional agents to an existing research group using the same filter DSL

        as recruitment. Existing members are preserved; only new agents are added.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_uuid
        required: true
        schema:
          type: string
        description: Research group UUID.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppendResearchGroupRequest'
      responses:
        '200':
          description: ResearchGroup updated with appended members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppendResearchGroupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/{group_id}:
    get:
      summary: Get a research group
      description: 'Returns research group metadata along with member agent profiles.

        Agent payloads include fields such as name, age, occupation, location, income,

        and other profile attributes used for recruitment and analysis.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
        description: Research group numeric ID or UUID.
      responses:
        '200':
          description: Research group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupDetailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update a research group
      description: 'Updates a group''s name and description.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
        description: Research group numeric ID or UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchGroupUpdateRequest'
      responses:
        '200':
          description: Research group updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupUpdateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    delete:
      summary: Archive a research group
      description: 'Archives a research group by deleting the group record while preserving related study history.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
        description: Research group numeric ID or UUID.
      responses:
        '200':
          description: Research group archived
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupArchiveResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups/{group_id}/agents/remove:
    post:
      summary: Remove agents from a research group
      description: 'Removes specific agents from a research group. Agents remain in the system;

        only the group membership is updated.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoveGroupAgentsRequest'
      responses:
        '200':
          description: Removal confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveGroupAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/{group_id}/agents/add:
    post:
      summary: Add agents to a research group by ID
      description: 'Adds specific recruitable agents to a research group. This endpoint is idempotent:

        agent IDs already in the group are reported in `already_member_agent_ids` and

        are not duplicated.

        '
      tags:
      - ResearchGroups
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
        description: Research group numeric ID or UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddGroupAgentsRequest'
      responses:
        '200':
          description: Add confirmation with per-ID outcomes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddGroupAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/filters:
    get:
      summary: Discover filter values for a country
      description: 'Returns country-scoped categorical filter values and counts from the recruitable

        agent pool (`description` present). This endpoint is intended to reduce filter-value

        guesswork before calling recruit/search/find endpoints.


        Country is required for v1 so values are scoped to one country''s taxonomy.

        '
      tags:
      - Filters
      parameters:
      - in: query
        name: country
        required: true
        schema:
          type: string
        description: 'Country scope for discovery (for example `US`, `USA`, `United Kingdom`, `Canada`).

          Alias handling matches recruitment filter behavior.

          '
      responses:
        '200':
          description: Country-scoped filter values and counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FilterDiscoveryResponse'
              example:
                country: US
                recruitable_agent_count: 412
                filters:
                  country:
                  - value: US
                    count: 412
                  state:
                  - value: CA
                    count: 56
                  - value: NY
                    count: 43
                  sex_at_birth:
                  - value: female
                    count: 215
                  - value: male
                    count: 197
                  labour_status:
                  - value: employed
                    count: 301
                  - value: Not in labor force
                    count: 72
                  religion:
                  - value: Catholic
                    count: 88
                  - value: Unaffiliated
                    count: 74
                  ethnicity:
                  - value: White
                    count: 190
                  - value: Hispanic or Latino
                    count: 94
        '400':
          description: Missing/invalid country input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missing_country:
                  summary: Country query param missing
                  value:
                    error:
                      code: invalid_request
                      message: country is required
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`groups:read`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/agents/search:
    get:
      summary: Search recruitable agents without creating a group
      description: 'Returns recruitable agents (`description` present) matching the same filter DSL

        semantics used for research-group recruitment. Results are paginated at 20 agents

        per page and include `total_count`.

        Supports either:

        - `filters` as JSON-encoded DSL payload, or

        - shorthand query fields using recruit filter aliases.

        List shorthand fields can be comma-separated and/or repeated query params.

        For exact values that contain commas (for example some `industry` labels), prefer

        the `filters` JSON payload for unambiguous matching.


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Recruitment Strategies](https://askditto.io/claude-code-guide/recruitment-strategies)

        '
      tags:
      - ResearchAgents
      parameters:
      - in: query
        name: page
        schema:
          type: integer
          minimum: 1
        description: 1-based page number. Defaults to 1.
      - in: query
        name: offset
        schema:
          type: integer
          minimum: 0
        description: Optional absolute offset. When provided, page is derived from offset.
      - in: query
        name: match
        schema:
          type: string
          enum:
          - all
          - any
        description: Filter matching mode when using shorthand query params.
      - in: query
        name: filters
        schema:
          type: string
        description: JSON-encoded filter payload using the recruit filter DSL (`{ match, filters }` or shorthand object).
      responses:
        '200':
          description: Agent search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSearchResponse'
              example:
                agents:
                - id: 9012
                  url: https://app.askditto.io/organization/org_123/agent/9012
                  uuid: ra_f2f4a9f1
                  name: Morgan Reed
                  age: 39
                  occupation: Account manager
                  industry: Retail
                  city: Chicago
                  state: IL
                  country: USA
                  sex: male
                  summary: Working parent who shops 2-3 times weekly.
                  annual_income_usd: 84000
                  marital_status: married
                  num_children: 2
                count: 1
                total_count: 412
                page: 1
                per_page: 20
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/agents/find:
    get:
      summary: Find one recruitable agent by filters
      description: 'Returns a single recruitable agent (`description` present) matching the same

        filter DSL semantics used for research-group recruitment.

        Supports either:

        - `filters` as JSON-encoded DSL payload, or

        - shorthand query fields using recruit filter aliases.

        List shorthand fields can be comma-separated and/or repeated query params.

        For exact values that contain commas (for example some `industry` labels), prefer

        the `filters` JSON payload for unambiguous matching.


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        '
      tags:
      - ResearchAgents
      parameters:
      - in: query
        name: match
        schema:
          type: string
          enum:
          - all
          - any
        description: Filter matching mode when using shorthand query params.
      - in: query
        name: filters
        schema:
          type: string
        description: JSON-encoded filter payload using the recruit filter DSL (`{ match, filters }` or shorthand object).
      responses:
        '200':
          description: Single agent match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFindOneResponse'
        '400':
          description: 'Invalid filter payload or invalid `filters` query encoding.

            Recovery:

            - Prefer JSON body (`POST /v1/agents/find`) for complex filters.

            - Use exact documented categorical values from `docs/api/guides/filters.md`.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: Invalid filters
                  details:
                    filters:
                      ethnicity:
                      - unsupported value; use an exact documented value
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`studies:read`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          description: No recruitable agent matched supplied filters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: not_found
                  message: No agent found for supplied filters
    post:
      summary: Find one recruitable agent by filters
      description: 'Returns a single recruitable agent (`description` present) matching recruitment

        filters passed in JSON body format.

        Accepts either:

        - a full filter object (`{ match, filters: {...} }`), or

        - shorthand fields (for example `{ country: "US", age_min: 65, sex: "male" }`).


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        '
      tags:
      - ResearchAgents
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentFindOneRequest'
            example:
              match: all
              country: USA
              state: TX
              age_min: 30
              age_max: 40
      responses:
        '200':
          description: Single agent match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFindOneResponse'
              example:
                agent:
                  id: 9012
                  url: https://app.askditto.io/organization/org_123/agent/9012
                  uuid: ra_f2f4a9f1
                  name: Morgan Reed
                  age: 39
                  occupation: Account manager
                  industry: Retail
                  city: Houston
                  state: TX
                  country: USA
                  sex: male
                  summary: Parent of two, regular grocery shopper.
                  annual_income_usd: 84000
                  marital_status: married
                  num_children: 2
                count: 1
                total_count: 47
        '400':
          description: 'Invalid filter payload.

            Recovery:

            - Validate keys/operators against the filters guide.

            - Use exact full-string categorical values.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: Invalid filters
                  details:
                    filters:
                      religion:
                      - unsupported value; expected one of documented values
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`studies:read`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          description: No recruitable agent matched supplied filters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: not_found
                  message: No agent found for supplied filters
  /v1/research-groups/{group_id}/questions:
    post:
      summary: Ask a question to a research group
      description: 'Sends a single question to a research group without creating a full study.

        This endpoint does not require creating a research study.

        Requires API key scope `studies:write`.

        If attachments are needed, upload files first via `POST /v1/media-assets` and pass

        `attachments: [{ "media_asset_id": "<id>" }]` in the request body.

        Polling caveat: finished job payloads for this endpoint may include sparse/empty

        `result` fields. If so, retrieve answers via study/group question listing endpoints.


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [Install Product Marketing Skill](https://askditto.io/claude-code/installing-product-marketing-skill)

        - [Why Ditto](https://askditto.io/claude-code/why-ditto)

        '
      tags:
      - ResearchGroups
      - GroupQuestions
      parameters:
      - in: path
        name: group_id
        required: true
        schema:
          type: string
        description: Research group numeric ID or UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupQuestionCreateRequest'
            example:
              question: What is your first reaction to this concept?
              attachments:
              - media_asset_id: ma_9f0a1c
      responses:
        '202':
          description: Group question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupQuestionBroadcastResponse'
              example:
                status: queued
                group_id: rg_3b9f2c0a
                question_id: 884
                job_ids:
                - job_001
                - job_002
                - job_003
                agent_count_targeted: 3
                status_url: https://app.askditto.io/v1/jobs/job_001
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-agents/{agent_id}/questions:
    post:
      summary: Ask a question to a research agent
      description: 'Sends a single question to a research agent outside of a study.

        This endpoint does not require creating a research study or research group.

        Requires API key scope `studies:write`.

        The agent must be recruitable (`description` present).

        If attachments are needed, upload files first via `POST /v1/media-assets` and pass

        `attachments: [{ "media_asset_id": "<id>" }]` in the request body.


        **Guides using this endpoint:**

        - [Install Ditto Skill](https://askditto.io/claude-code/installing-ditto-skill)

        - [Install Product Marketing Skill](https://askditto.io/claude-code/installing-product-marketing-skill)

        - [Why Ditto](https://askditto.io/claude-code/why-ditto)

        '
      tags:
      - ResearchAgents
      - AgentQuestions
      parameters:
      - in: path
        name: agent_id
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentQuestionCreateRequest'
            example:
              question: What do you think about this product idea?
              attachments:
              - media_asset_id: ma_9f0a1c
      responses:
        '202':
          description: Agent question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentQuestionResponse'
              example:
                status: queued
                agent_id: 9012
                question_id: 4421
                job_id: job_abc123
                status_url: https://app.askditto.io/v1/jobs/job_abc123
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-study-requests:
    post:
      summary: Create or resume a natural-language research study request
      description: 'AI-assisted study orchestration workflow. Describe your study objective in plain

        English and Ditto orchestrates group selection, question planning, and study setup.

        New requests use `request_text`; resumes use `request_id` + `additional_text`.


        This endpoint follows a conversational request protocol:

        - `202`: queued for async processing

        - `400`: additional clarification needed before continuing

        - `409`: review checkpoint requiring explicit user action

        '
      tags:
      - ResearchStudies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchStudyRequestCreateOrResumeRequest'
      responses:
        '202':
          description: Request queued for async processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyRequestQueuedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-studies:
    get:
      summary: List research studies
      description: 'Returns research studies for the organization tied to the API key.

        Results include participant and question counts.

        Paginated via `limit` and `offset`.

        '
      tags:
      - ResearchStudies
      parameters:
      - in: query
        name: limit
        schema:
          type: integer
          minimum: 1
          maximum: 500
      - in: query
        name: offset
        schema:
          type: integer
          minimum: 0
      responses:
        '200':
          description: ResearchStudy list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Create a research study
      description: 'Creates a research study and links it to a recruited research group.


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Customer Segmentation](https://askditto.io/claude-code-guide/customer-segmentation-guide)

        - [Sales Enablement](https://askditto.io/claude-code-guide/sales-enablement-guide)

        - [Voice of Customer](https://askditto.io/claude-code-guide/voice-of-customer-guide)

        - [GTM Strategy Validation](https://askditto.io/claude-code-guide/gtm-strategy-validation-guide)

        - [Product Launch Research](https://askditto.io/claude-code-guide/product-launch-research-guide)

        '
      x-workflow-step: 2
      x-codeSamples:
      - lang: cURL
        label: Create Study
        source: "curl -X POST \"$BASE_URL/v1/research-studies\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -d '{\n    \"title\": \"Grocery Preferences 2026\",\n    \"objective\": \"Understand\
          \ grocery shopping priorities\",\n    \"research_group_uuid\": \"<group_uuid>\"\n  }'\n"
      tags:
      - ResearchStudies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchStudyCreateRequest'
            example:
              title: Grocery Preferences 2026
              objective: Understand grocery shopping priorities
              research_group_uuid: rg_3b9f2c0a
              shareable: false
      responses:
        '201':
          description: ResearchStudy created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyResponse'
              example:
                study:
                  id: 812
                  uuid: rs_aa1d9b2f
                  title: Grocery Preferences 2026
                  objective: Understand grocery shopping priorities
                  shareable: false
                  research_group_uuid: rg_3b9f2c0a
                  share_url: null
                  share_link: null
                  url: https://app.askditto.io/organization/org_123/studies/rs_aa1d9b2f
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}:
    get:
      summary: Get a research study
      description: Returns a single research study for the organization tied to the API key.
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: ResearchStudy details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a research study
      description: Deletes a study and its related questions/answers. The research group remains untouched.
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: ResearchStudy deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/agents/remove:
    post:
      summary: Remove agents from a research study
      description: 'Removes specific agents from a research study. Agents remain in their research groups;

        only the study membership is updated.

        '
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoveStudyAgentsRequest'
      responses:
        '200':
          description: Removal confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveStudyAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-studies/{study_id}/questions:
    get:
      summary: List study questions with answers
      description: 'Returns all questions for the study with any collected answers. Answers arrive asynchronously,

        so newly created questions may return an empty `answers` list until responses are stored.


        Questions are ordered by creation time (newest first). Answers are ordered by submission time.

        '
      tags:
      - ResearchStudies
      - StudyQuestions
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: Study questions with answers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyQuestionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Create and broadcast a study question
      x-workflow-step: 3
      x-codeSamples:
      - lang: cURL
        label: Ask Study Question
        source: "curl -X POST \"$BASE_URL/v1/research-studies/<study_id>/questions\" \\\n  -H \"Authorization: Bearer $API_KEY\"\
          \ \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"question\": \"What matters most when choosing a\
          \ grocery store?\"\n  }'\n"
      description: 'Broadcasts one question to the targeted participants in a study.

        Requires API key scope `studies:write`.

        For attachments, upload files first via `POST /v1/media-assets` and pass

        `attachments: [{ "media_asset_id": "<id>" }]`.


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Question Design Playbook](https://askditto.io/claude-code-guide/question-design-playbook)

        - [Product Messaging](https://askditto.io/claude-code-guide/test-product-messaging-guide)

        - [Positioning Validation](https://askditto.io/claude-code-guide/validate-product-positioning-guide)

        '
      tags:
      - ResearchStudies
      - StudyQuestions
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyQuestionCreateRequest'
            example:
              question: What matters most when choosing a grocery store?
              attachments:
              - media_asset_id: ma_9f0a1c
      responses:
        '202':
          description: Broadcast queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyQuestionBroadcastResponse'
              example:
                status: queued
                study_id: 812
                question_id: 4421
                job_ids:
                - job_001
                - job_002
                agent_ids:
                - 9012
                - 9013
                agents:
                - id: 9012
                  url: https://app.askditto.io/organization/org_123/agent/9012
                - id: 9013
                  url: https://app.askditto.io/organization/org_123/agent/9013
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/complete:
    post:
      summary: Trigger study completion workflow
      x-workflow-step: 5
      x-codeSamples:
      - lang: cURL
        label: Complete Study
        source: "curl -X POST \"$BASE_URL/v1/research-studies/<study_id>/complete\" \\\n  -H \"Authorization: Bearer $API_KEY\"\
          \ \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"force\": false\n  }'\n"
      description: 'Finalizes a study and runs the completion analysis pipeline. This action is irreversible and

        should only be called after data collection is complete.


        The completion pipeline generates:

        - Summary: a concise synthesis of the study and question responses.

        - Insights: key themes, notable patterns, and standout participant perspectives.

        - Correlations: relationships between response patterns and participant attributes (when available).

        - Final takeaways: short, action-oriented conclusions for reporting.


        When the queued job reaches `finished` (via `GET /v1/jobs/{job_id}`), `result`

        includes structured completion outputs such as:

        - `overall_summary`

        - `key_segments`

        - `divergences`

        - `shared_mindsets`

        - `next_questions`

        - `stats`


        Processing is asynchronous and typically takes 10-15 minutes. Use the job status endpoint

        to monitor completion.

        If a completion step is already done and `force` is false, that step is skipped and may

        not appear in `queued_steps`.


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Customer Segmentation](https://askditto.io/claude-code-guide/customer-segmentation-guide)

        - [Competitive Intelligence](https://askditto.io/claude-code-guide/competitive-intelligence-guide)

        - [Pricing Research](https://askditto.io/claude-code-guide/pricing-research-guide)

        '
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyCompletionRequest'
            example:
              force: false
      responses:
        '202':
          description: Workflow queued (async). Returns job IDs and the steps queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyCompletionResponse'
              example:
                job_ids:
                - job_comp_001
                queued_steps:
                - study_correlations
                - study_recommendations
                - study_research_summary
                - study_tldr
        '400':
          description: 'Invalid completion payload.

            Recovery:

            - Send `steps` as a list when provided.

            - Use only supported workflow step names.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                steps_not_list:
                  summary: steps was not an array
                  value:
                    error:
                      code: invalid_request
                      message: steps must be a list
                unknown_steps:
                  summary: Unsupported step names supplied
                  value:
                    error:
                      code: invalid_request
                      message: Unknown step(s)
                      details:
                        steps:
                        - study_magic
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`studies:write`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/share:
    get:
      summary: Get study share info
      description: 'Returns the current share settings and shareable link for a study.

        If sharing is enabled, the URL can be opened by anyone with the link

        (no authentication required) and shows the generated report.

        '
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: Share info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Update study share settings
      x-workflow-step: 6
      x-codeSamples:
      - lang: cURL
        label: Enable Share Link
        source: "curl -X POST \"$BASE_URL/v1/research-studies/<study_id>/share\" \\\n  -H \"Authorization: Bearer $API_KEY\"\
          \ \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"enabled\": true\n  }'\n"
      description: 'Enables or disables a public share link for the study. When enabled,

        the response includes a URL that can be opened by anyone with the link

        to view the generated report. Treat the link like a secret.

        Optional tracking convention: append UTM parameters such as

        `?utm_source=ce` (cold email) or `?utm_source=blog` (content/blog links).


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Content Marketing Engine](https://askditto.io/claude-code-guide/content-marketing-engine-guide)

        - [Case Study Library](https://askditto.io/claude-code-guide/case-study-library)

        '
      tags:
      - ResearchStudies
      parameters:
      - in: path
        name: study_id
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShareUpdateRequest'
            example:
              enabled: true
              regenerate: false
      responses:
        '200':
          description: Share info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareInfo'
              example:
                enabled: true
                url: https://app.askditto.io/organization/studies/shared/shr_8a0f91
                token: shr_8a0f91
                expires_at: null
                is_expired: false
        '400':
          description: 'Invalid share settings payload.

            Recovery:

            - Provide `expires_at`/`shareable_until` as valid ISO-8601 datetime.

            - If sharing is enabled, expiration must be in the future.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalid_timestamp:
                  summary: Expiration timestamp parse failed
                  value:
                    error:
                      code: invalid_request
                      message: Invalid expiration timestamp
                expiration_in_past:
                  summary: Expiration provided in the past
                  value:
                    error:
                      code: invalid_request
                      message: Expiration must be in the future
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key is valid but lacks required scope (`studies:write`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: forbidden
                  message: Forbidden
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys:
    post:
      summary: Create a Zeitgeist survey
      tags:
      - Zeitgeist
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZeitgeistSurveyCreateRequest'
      responses:
        '201':
          description: Zeitgeist survey created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys/{survey_id}:
    delete:
      summary: Delete a Zeitgeist survey
      description: Deletes a survey and its associated answers/results.
      tags:
      - Zeitgeist
      parameters:
      - in: path
        name: survey_id
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: Zeitgeist survey deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys/{survey_id}/results:
    get:
      summary: List Zeitgeist survey results
      description: Returns survey result snapshots with pagination and ordering.
      tags:
      - Zeitgeist
      parameters:
      - in: path
        name: survey_id
        required: true
        schema:
          type: integer
      - in: query
        name: page
        schema:
          type: integer
          minimum: 1
      - in: query
        name: page_size
        schema:
          type: integer
          minimum: 1
          maximum: 500
      - in: query
        name: order_by
        schema:
          type: string
          enum:
          - created_at
          - period_date
          - id
          - title
      - in: query
        name: order
        schema:
          type: string
          enum:
          - asc
          - desc
      responses:
        '200':
          description: Zeitgeist survey results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyResultListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/jobs/{job_id}:
    get:
      summary: Get job status
      x-workflow-step: 4
      x-codeSamples:
      - lang: cURL
        label: Poll Job
        source: "curl -X GET \"$BASE_URL/v1/jobs/<job_id>\" \\\n  -H \"Authorization: Bearer $API_KEY\"\n"
      description: 'Returns asynchronous job state for question, completion, and free-tier workflows.

        Common values:

        - `started`: still processing

        - `finished`: result available

        - `failed`: check `error` for details


        **Guides using this endpoint:**

        - [API Workflow Hub](https://askditto.io/claude-code-guide)

        - [Customer Segmentation](https://askditto.io/claude-code-guide/customer-segmentation-guide)

        - [Voice of Customer](https://askditto.io/claude-code-guide/voice-of-customer-guide)

        - [Sales Enablement](https://askditto.io/claude-code-guide/sales-enablement-guide)

        '
      tags:
      - Jobs
      parameters:
      - in: path
        name: job_id
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
              examples:
                started:
                  summary: Job is still processing
                  value:
                    status: started
                    result: null
                    error: null
                    meta: {}
                finished_study_question:
                  summary: Study question answer completed
                  value:
                    status: finished
                    result:
                      response_text: Convenience and price matter most to me.
                      agent_id: 9012
                    error: null
                    meta: {}
                finished_study_completion:
                  summary: Study completion pipeline finished
                  value:
                    status: finished
                    result:
                      overall_summary: Price and convenience are the dominant decision drivers.
                      key_segments:
                      - segment: Budget-focused households
                        insight: Prioritize promotions and predictable pricing.
                      divergences:
                      - topic: Delivery fees
                        split: Some users pay for convenience; others avoid all added fees.
                      shared_mindsets:
                      - value: Time savings
                      next_questions:
                      - How much fee is acceptable for same-day delivery?
                      stats:
                        participant_count: 10
                        answered_count: 10
                    error: null
                    meta: {}
                finished_group_question_sparse_result:
                  summary: Group question job with sparse result payload
                  value:
                    status: finished
                    result:
                      agent_id: null
                      response_text: ''
                    error: null
                    meta: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: 'Authenticated request cannot access this job.

            Recovery:

            - Ensure API key has the required scope for the underlying job resource.

            - Ensure the job belongs to the same organization as the key.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                insufficient_scope:
                  summary: Missing scope for this job type
                  value:
                    error:
                      code: forbidden
                      message: Insufficient scope
                cross_org_job:
                  summary: Job belongs to another organization
                  value:
                    error:
                      code: forbidden
                      message: Job not accessible for this key
        '404':
          description: Job ID format or value is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: not_found
                  message: Invalid job_id
        '500':
          description: Job failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
              example:
                status: failed
                result: null
                error: timeout
                meta: {}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: APIKey
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableEntity:
      description: Semantic validation failure for otherwise well-formed input.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    CliAuthStartRequest:
      type: object
      required:
      - redirect_uri
      properties:
        provider:
          type: string
          enum:
          - google
          description: OAuth provider (defaults to google).
        redirect_uri:
          type: string
          description: Loopback redirect URI (localhost or 127.0.0.1).
    CliAuthStartResponse:
      type: object
      properties:
        ok:
          type: boolean
        session_id:
          type: string
        authorization_url:
          type: string
        expires_in:
          type: integer
      required:
      - ok
      - session_id
      - authorization_url
      - expires_in
    CliAuthCompleteRequest:
      type: object
      required:
      - session_id
      - code
      - state
      properties:
        session_id:
          type: string
        code:
          type: string
        state:
          type: string
    CliAuthCompleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: string
          enum:
          - complete
        api_key:
          type: string
        org_uuid:
          type: string
        next_steps:
          $ref: '#/components/schemas/CliAuthNextSteps'
      required:
      - ok
      - status
      - api_key
      - org_uuid
    CliAuthLoginRequiredResponse:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: string
          enum:
          - login_required
        login_url:
          type: string
        next_steps:
          $ref: '#/components/schemas/CliAuthNextSteps'
      required:
      - ok
      - status
      - login_url
    CliAuthNextSteps:
      type: object
      properties:
        summary:
          type: string
        docs_url:
          type: string
    RecruitResearchGroupRequest:
      type: object
      required:
      - name
      - filters
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        filters:
          allOf:
          - $ref: '#/components/schemas/RecruitFilters'
          description: 'Accepts the full filter object (`{ match, filters: {...} }`) or a shorthand

            filter map with fields directly (for example: `{ country: "UK", age_min: 45, age_max: 60 }`).

            '
        group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Requested group size (1-20).
        sample_method:
          type: string
          enum:
          - random
          - stratified
        dedupe:
          type: boolean
          default: true
    CreateResearchGroupFromAgentsRequest:
      type: object
      required:
      - name
      - agent_ids
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        dedupe:
          type: boolean
          default: true
        agent_ids:
          type: array
          items:
            type: integer
    RecruitFilters:
      type: object
      required:
      - filters
      properties:
        match:
          type: string
          enum:
          - all
          - any
          default: all
          description: 'Controls how multiple filter fields are combined:


            - all: every filter field must match (logical AND across fields)

            - any: at least one filter field must match (logical OR across fields)


            Note: For list filters, the list''s `any`/`all` applies within that single field,

            while `match` applies across different fields.

            '
        filters:
          type: object
          description: "Supported input formats:\n- Nested DSL payload:\n  `{ \"match\": \"all\", \"filters\": { \"country\"\
            : { \"any\": [\"USA\"] }, \"age\": { \"min\": 25, \"max\": 45 } } }`\n- Flat shorthand fields:\n  `{ \"match\"\
            : \"all\", \"country\": \"USA\", \"state\": \"CA\", \"age_min\": 25, \"age_max\": 45 }`\n\nFilterable properties\
            \ and types:\n- age: range (min/max)\n- income_annual_usd: range (min/max)\n- income_annual_local_currency: range\
            \ (min/max)\n- state: list (any/all)\n- city: list (any/all)\n- country: list (any/all)\n- description: text (contains)\n\
            - religion: list (any/all)\n- industry: list (any/all)\n- occupation: list (any/all)\n- labour_status: list (any/all)\n\
            - ethnicity: list (any/all)\n- sex_at_birth: list (any/all)\n- is_parent: boolean\n- is_female: boolean\n\nFixed-value\
            \ references:\n- country: USA, UK, Germany, Canada\n- sex_at_birth: male, female\n- labour_status: employed, unemployed,\
            \ \"Not in labor force\"\n- religion: Catholic, Protestant, Jewish, Muslim, Buddhist, Hindu, Unaffiliated\n\n\
            Matching and behavior notes:\n- List/string filter values are case-insensitive.\n- List filters still require\
            \ exact full-string values (no partial/fuzzy matching).\n- Range filters accept min and/or max (integers).\n-\
            \ Boolean filters accept true/false.\n- Text filters accept `contains` and match case-insensitive substrings.\n\
            - Shorthand range keys (`age_min`, `age_max`, `income_annual_usd_min`, etc.)\n  are accepted when provided alongside\
            \ other filter fields.\n- Shorthand text keys (`description_contains`) are accepted in the same way.\n- `description`\
            \ is reserved for the ResearchGroup description on recruit requests,\n  so use nested filters or `description_contains`\
            \ when filtering by agent description.\n- `state` should use 2-letter state or province codes for US/Canada\n\
            \  (for example `CA`, `NY`, `ON`).\n- `country` matches against both `country` and `nation` fields and accepts\n\
            \  common aliases (for example `UK`, `GB`, `United Kingdom`, `US`, `USA`).\n            Notes:\n            -\
            \ `political_party` and `gender_identity` are currently unpopulated and are omitted from the public filter docs\
            \ until data is populated."
          additionalProperties:
            $ref: '#/components/schemas/FilterValue'
    FilterValue:
      oneOf:
      - $ref: '#/components/schemas/RangeFilter'
      - $ref: '#/components/schemas/ListFilter'
      - $ref: '#/components/schemas/TextFilter'
      - type: boolean
      - type: string
    RangeFilter:
      type: object
      properties:
        min:
          type: number
        max:
          type: number
    ListFilter:
      type: object
      properties:
        any:
          type: array
          items:
            type: string
        all:
          type: array
          items:
            type: string
    TextFilter:
      type: object
      properties:
        contains:
          type: string
    ResearchGroup:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_count:
          type: integer
        created_at:
          type: string
          format: date-time
        url:
          type: string
          nullable: true
          format: uri
    ResearchGroupListResponse:
      type: object
      properties:
        groups:
          type: array
          items:
            $ref: '#/components/schemas/ResearchGroup'
        count:
          type: integer
        total:
          type: integer
    AgentLink:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
          nullable: true
          format: uri
    ResearchGroupAgentDetail:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
          nullable: true
          format: uri
        name:
          type: string
        age:
          type: integer
        occupation:
          type: string
          nullable: true
        industry:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        sex:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        annual_income_usd:
          type: integer
          nullable: true
        marital_status:
          type: string
          nullable: true
        num_children:
          type: integer
          nullable: true
    ResearchGroupDetail:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_count:
          type: integer
        agents:
          type: array
          items:
            $ref: '#/components/schemas/ResearchGroupAgentDetail'
    ResearchGroupDetailResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroupDetail'
    ResearchGroupManualAgent:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
          nullable: true
          format: uri
        uuid:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        age:
          type: integer
          nullable: true
        occupation:
          type: string
          nullable: true
        industry:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        sex:
          type: string
          nullable: true
        annual_income_usd:
          type: integer
          nullable: true
        marital_status:
          type: string
          nullable: true
        num_children:
          type: integer
          nullable: true
    ResearchGroupManualDetail:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_count:
          type: integer
        url:
          type: string
          nullable: true
          format: uri
        agents:
          type: array
          items:
            $ref: '#/components/schemas/ResearchGroupManualAgent'
    CreateResearchGroupFromAgentsResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroupManualDetail'
        added_agent_ids:
          type: array
          items:
            type: integer
        added_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        added_count:
          type: integer
        not_found_agent_ids:
          type: array
          items:
            type: integer
        invalid_agent_ids:
          type: array
          items:
            oneOf:
            - type: integer
            - type: string
            - type: boolean
            - type: number
            - type: object
              additionalProperties: true
            - type: array
              items: {}
    RecruitResearchGroupResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        filters:
          $ref: '#/components/schemas/RecruitFilters'
        recruited_agent_ids:
          type: array
          items:
            type: integer
        recruited_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
      example:
        group:
          id: 42
          uuid: rg_123abc
          name: California parents
          description: Parents in CA with kids under 10
          agent_count: 250
        filters:
          match: all
          filters:
            state:
              any:
              - CA
            age:
              min: 25
              max: 45
    InterviewResearchGroupRequest:
      type: object
      required:
      - objective
      properties:
        objective:
          type: string
          description: Natural-language objective used to find candidate participants.
        question:
          type: string
          description: Alias for objective.
        name:
          type: string
        description:
          type: string
          nullable: true
        filters:
          allOf:
          - $ref: '#/components/schemas/RecruitFilters'
          description: Optional filter constraints applied before interview scoring.
        group_size:
          type: integer
          minimum: 1
          description: Desired final group size; defaults to 200 when omitted.
        research_group_id:
          type: integer
          minimum: 1
          description: Optional candidate group ID to refine instead of net-new recruitment.
        dedupe:
          type: boolean
          default: true
    InterviewResearchGroupResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - queued
        job_id:
          type: string
        status_url:
          type: string
          format: uri
        objective:
          type: string
        target_size:
          type: integer
        interview_cap:
          type: integer
        filters:
          $ref: '#/components/schemas/RecruitFilters'
        group:
          type: object
          nullable: true
          properties:
            id:
              type: integer
              nullable: true
            uuid:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
            url:
              type: string
              nullable: true
              format: uri
        candidate_group_id:
          type: integer
          nullable: true
      required:
      - status
      - job_id
      - status_url
    ResearchGroupRequestCreateRequest:
      type: object
      required:
      - request_text
      properties:
        request_text:
          type: string
          description: Freeform audience description used to derive recruitment filters.
        group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Requested group size (1-20). Defaults to 10 unless over-recruiting is needed.
        sample_method:
          type: string
          enum:
          - random
          - stratified
        dedupe:
          type: boolean
          default: true
        response_mode:
          type: string
          enum:
          - sync
          - async
          description: Return immediately (sync) or queue background processing (async).
        reuse_group_uuid:
          type: string
          description: Optional existing group UUID to reuse when appropriate.
      description: "Guardrails:\n- Country support is limited to USA, UK, Germany, and Canada (aliases normalized).\n- US\
        \ states must use 2-letter codes (for example CA, NY).\n- This endpoint's natural-language parser rejects filters\
        \ such as\n  income, city, ethnicity, political_affiliation, and location.\n- For explicit city targeting, use filter-DSL\
        \ endpoints such as\n  `/v1/research-groups/recruit` or `/v1/agents/find`.\n- When targeting niche cohorts, the service\
        \ may over-recruit (15-20) and return a\n  screening question for later curation.\n"
    ResearchGroupRequestResumeRequest:
      type: object
      required:
      - request_id
      properties:
        request_id:
          type: string
        additional_text:
          type: string
          description: Follow-up details or clarifications from the requester.
        review_action:
          type: string
          enum:
          - continue
          - relax_filters
          - remove_agents
          - finalize
          description: How to proceed after a review checkpoint.
        remove_agent_ids:
          type: array
          items:
            type: integer
          description: Optional agent IDs to remove from the group before continuing.
        target_group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Optional override for the desired final group size.
    ResearchGroupRequestCompletedResponse:
      type: object
      properties:
        request_id:
          type: string
        status:
          type: string
          enum:
          - completed
        phase:
          type: string
          nullable: true
        progress_message:
          type: string
          nullable: true
        group:
          $ref: '#/components/schemas/ResearchGroup'
        filters:
          $ref: '#/components/schemas/RecruitFilters'
        warnings:
          type: array
          items:
            type: string
        screening_question:
          type: string
          nullable: true
        attempt_count:
          type: integer
        tasks:
          type: array
          items:
            type: string
        latest_tasks:
          type: array
          nullable: true
          items:
            type: string
        loop:
          type: integer
          nullable: true
        recruited_agent_ids:
          type: array
          items:
            type: integer
        recruited_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
      required:
      - request_id
      - status
      - group
    ResearchGroupRequestQueuedResponse:
      type: object
      properties:
        request_id:
          type: string
        job_id:
          type: string
        status_url:
          type: string
      required:
      - request_id
      - job_id
      - status_url
    ResearchGroupRequestClarificationResponse:
      type: object
      properties:
        error:
          type: string
          enum:
          - needs_clarification
        request_id:
          type: string
        phase:
          type: string
          nullable: true
        progress_message:
          type: string
          nullable: true
        clarifying_questions:
          type: array
          items:
            type: string
        missing_info:
          type: array
          items:
            type: string
        proposed_filters:
          $ref: '#/components/schemas/RecruitFilters'
        warnings:
          type: array
          items:
            type: string
        tasks:
          type: array
          items:
            type: string
        latest_tasks:
          type: array
          nullable: true
          items:
            type: string
        loop:
          type: integer
          nullable: true
        recruited_agent_ids:
          type: array
          items:
            type: integer
        recruited_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
      required:
      - error
      - request_id
      - clarifying_questions
    ResearchGroupRequestReviewResponse:
      type: object
      properties:
        error:
          type: string
          enum:
          - needs_review
        request_id:
          type: string
        phase:
          type: string
          nullable: true
        progress_message:
          type: string
          nullable: true
        attempt:
          type: integer
        group:
          $ref: '#/components/schemas/ResearchGroup'
        filters_used:
          $ref: '#/components/schemas/RecruitFilters'
        suggested_relaxations:
          type: array
          items:
            type: string
        warnings:
          type: array
          items:
            type: string
        tasks:
          type: array
          items:
            type: string
        latest_tasks:
          type: array
          nullable: true
          items:
            type: string
        loop:
          type: integer
          nullable: true
        recruited_agent_ids:
          type: array
          items:
            type: integer
        recruited_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
      required:
      - error
      - request_id
      - attempt
      - group
    ResearchGroupRequestStatusResponse:
      type: object
      properties:
        request_id:
          type: string
        status:
          type: string
          enum:
          - queued
          - running
          - needs_clarification
          - awaiting_review
          - completed
          - failed
        phase:
          type: string
          nullable: true
        progress_message:
          type: string
          nullable: true
        group:
          $ref: '#/components/schemas/ResearchGroup'
        last_filters:
          $ref: '#/components/schemas/RecruitFilters'
        attempt_count:
          type: integer
        warnings:
          type: array
          items:
            type: string
        screening_question:
          type: string
          nullable: true
        tasks:
          type: array
          items:
            type: string
        latest_tasks:
          type: array
          nullable: true
          items:
            type: string
        loop:
          type: integer
          nullable: true
        recruited_agent_ids:
          type: array
          items:
            type: integer
        recruited_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
      - request_id
      - status
    AppendResearchGroupRequest:
      type: object
      properties:
        filters:
          allOf:
          - $ref: '#/components/schemas/RecruitFilters'
          description: 'Accepts the full filter object (`{ match, filters: {...} }`) or a shorthand

            filter map with fields directly (for example: `{ country: "UK", age_min: 45, age_max: 60 }`).

            '
        group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Requested number of agents to append (1-20).
    AppendResearchGroupResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        appended:
          type: integer
          description: Number of agents added to the group.
        appended_agent_ids:
          type: array
          items:
            type: integer
        appended_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        filters:
          $ref: '#/components/schemas/RecruitFilters'
      example:
        group:
          id: 42
          uuid: rg_123abc
          name: California parents
          description: Parents in CA with kids under 10
          agent_count: 255
        appended: 5
        filters:
          match: all
          filters:
            state:
              any:
              - CA
            age:
              min: 25
              max: 45
    ResearchGroupUpdateRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
      description: 'Update group metadata.

        At least one of `name` or `description` is required.

        Set `description` to null to clear it.

        '
    ResearchGroupUpdateResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        updated:
          type: object
          properties:
            name:
              type: boolean
            description:
              type: boolean
    ResearchGroupArchiveResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        archived:
          type: boolean
        linked_study_count:
          type: integer
    FreeTierQuestionCreateRequest:
      type: object
      required:
      - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    FreeTierQuestionBroadcastResponse:
      type: object
      properties:
        ok:
          type: boolean
        group_id:
          type: integer
        count:
          type: integer
        job_id:
          type: string
        agent_ids:
          type: array
          items:
            type: integer
        agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        agent_uuids:
          type: array
          items:
            type: string
        question:
          type: string
    GroupQuestionCreateRequest:
      type: object
      description: 'Request payload for direct group questioning (`POST /v1/research-groups/{group_id}/questions`).

        If files are included, `media_asset_id` is recommended (upload first via `POST /v1/media-assets`).

        '
      required:
      - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    GroupQuestionBroadcastResponse:
      type: object
      properties:
        status:
          type: string
        group_id:
          type: string
        question_id:
          type: integer
        job_ids:
          type: array
          items:
            type: string
        agent_count_targeted:
          type: integer
        status_url:
          type: string
          format: uri
    DirectAnswer:
      type: object
      properties:
        response_text:
          type: string
        submitted_at:
          type: string
          format: date-time
          nullable: true
        agent_id:
          type: integer
          nullable: true
        agent_uuid:
          type: string
          nullable: true
        agent_name:
          type: string
          nullable: true
    AgentSearchResult:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
          nullable: true
          format: uri
        uuid:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        age:
          type: integer
          nullable: true
        occupation:
          type: string
          nullable: true
        industry:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        sex:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        annual_income_usd:
          type: integer
          nullable: true
        marital_status:
          type: string
          nullable: true
        num_children:
          type: integer
          nullable: true
    AgentSearchResponse:
      type: object
      properties:
        agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentSearchResult'
        count:
          type: integer
        total_count:
          type: integer
        page:
          type: integer
        per_page:
          type: integer
    AgentFindOneRequest:
      type: object
      additionalProperties: true
      description: 'Accepts the same filter semantics as recruitment/search:

        - full filter object (`{ match, filters: {...} }`), or

        - shorthand filter fields (`country`, `state`, `industry`, `age_min`, etc.).

        '
    AgentFindOneResponse:
      type: object
      properties:
        agent:
          $ref: '#/components/schemas/AgentSearchResult'
        count:
          type: integer
        total_count:
          type: integer
      required:
      - agent
      - count
      - total_count
    FilterValueCount:
      type: object
      properties:
        value:
          type: string
        count:
          type: integer
      required:
      - value
      - count
    FilterDiscoveryFilterSet:
      type: object
      properties:
        country:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
        state:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
        sex_at_birth:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
        labour_status:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
        religion:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
        ethnicity:
          type: array
          items:
            $ref: '#/components/schemas/FilterValueCount'
      required:
      - country
      - state
      - sex_at_birth
      - labour_status
      - religion
      - ethnicity
    FilterDiscoveryResponse:
      type: object
      properties:
        country:
          type: string
        recruitable_agent_count:
          type: integer
        filters:
          $ref: '#/components/schemas/FilterDiscoveryFilterSet'
      required:
      - country
      - recruitable_agent_count
      - filters
    AgentQuestionCreateRequest:
      type: object
      description: 'Request payload for direct single-agent questioning (`POST /v1/research-agents/{agent_id}/questions`).

        If files are included, `media_asset_id` is recommended (upload first via `POST /v1/media-assets`).

        '
      required:
      - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    AgentQuestionResponse:
      type: object
      properties:
        status:
          type: string
        agent_id:
          type: integer
        question_id:
          type: integer
        job_id:
          type: string
        status_url:
          type: string
          format: uri
        answer:
          allOf:
          - $ref: '#/components/schemas/DirectAnswer'
          nullable: true
    ResearchStudyRequestCreateOrResumeRequest:
      type: object
      properties:
        request_text:
          type: string
          description: Required when creating a new request.
        request_id:
          type: string
          description: Required when resuming an existing request.
        additional_text:
          type: string
          description: Follow-up context when resuming an existing request.
        sender_context:
          type: object
          additionalProperties: true
          description: Optional caller metadata merged into request context.
        llm_overrides:
          type: object
          additionalProperties: true
          description: Optional model/runtime overrides for orchestration.
        question_count:
          type: integer
          minimum: 1
      oneOf:
      - required:
        - request_text
      - required:
        - request_id
        - additional_text
    ResearchStudyRequestQueuedResponse:
      type: object
      properties:
        request_id:
          type: string
        job_id:
          type: string
        status_url:
          type: string
          format: uri
      required:
      - request_id
      - job_id
      - status_url
    ResearchStudyCreateRequest:
      type: object
      required:
      - title
      - objective
      properties:
        title:
          type: string
        objective:
          type: string
        shareable:
          type: boolean
        research_group_uuid:
          type: string
    MediaAssetUploadRequest:
      type: object
      properties:
        filename:
          type: string
        name:
          type: string
        mime:
          type: string
          description: 'Allowed values: image/png, image/jpeg, image/gif, image/webp, application/pdf.'
        encoding:
          type: string
          enum:
          - base64
        data_url:
          type: string
          description: Data URL with base64 payload (for example data:image/png;base64,...).
        file_data:
          type: string
          description: Base64-encoded file contents.
      description: 'Upload one image/PDF and receive a reusable media asset id for question attachments.

        Provide either `file_data` (base64) or `data_url`.

        '
    MediaAssetUpload:
      type: object
      properties:
        id:
          type: string
        filename:
          type: string
        mime:
          type: string
        size_bytes:
          type: integer
        width:
          type: integer
          nullable: true
        height:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        url:
          type: string
          format: uri
          nullable: true
    MediaAssetUploadResponse:
      type: object
      properties:
        media_asset:
          $ref: '#/components/schemas/MediaAssetUpload'
    QuestionAttachmentInput:
      type: object
      properties:
        name:
          type: string
        filename:
          type: string
        label:
          type: string
        mime:
          type: string
          description: 'Allowed values: image/png, image/jpeg, image/gif, image/webp, application/pdf.'
        encoding:
          type: string
          enum:
          - base64
        data_url:
          type: string
          description: Data URL with base64 payload (for example data:image/png;base64,...).
        file_data:
          type: string
          description: Base64-encoded file contents.
        file_id:
          type: string
          description: Provider file ID (supported for direct/group/free-tier questions).
        file_url:
          type: string
          format: uri
          description: HTTPS URL to a file (supported for direct/group/free-tier questions).
        media_asset_id:
          type: string
          description: Existing media asset ID (supported for direct, free-tier, and study questions).
      description: 'Attach files or images to a question. Allowed types are images (PNG/JPEG/GIF/WEBP) and PDFs.

        For all question endpoints, `media_asset_id` is recommended (upload via `POST /v1/media-assets`).

        For study questions, use `data_url`, `file_data` (base64), or `media_asset_id`.

        For direct/group/free-tier questions, `file_id` or `file_url` is also accepted; include

        `mime` (or a filename with a supported extension) so the API can validate the file type.

        '
    QuestionAttachment:
      type: object
      properties:
        name:
          type: string
        label:
          type: string
        media_asset_id:
          type: string
        preview_url:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        mime:
          type: string
        width:
          type: integer
          nullable: true
        height:
          type: integer
          nullable: true
    ResearchStudy:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        title:
          type: string
        objective:
          type: string
        shareable:
          type: boolean
        organization_id:
          type: integer
        research_group_uuid:
          type: string
          nullable: true
        research_group_url:
          type: string
          nullable: true
          format: uri
        share_url:
          type: string
          nullable: true
          format: uri
        share_link:
          type: string
          nullable: true
          format: uri
        url:
          type: string
          nullable: true
          format: uri
        created_at:
          type: string
          format: date-time
    ResearchStudyResponse:
      type: object
      properties:
        study:
          $ref: '#/components/schemas/ResearchStudy'
      example:
        study:
          id: 101
          uuid: rs_456def
          title: Q1 caregiver needs
          objective: Understand caregiver priorities in 2026
          shareable: false
          research_group_uuid: rg_123abc
    ResearchStudyListItem:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        title:
          type: string
        objective:
          type: string
        stage:
          type: string
          nullable: true
        shareable:
          type: boolean
        share_url:
          type: string
          nullable: true
        share_link:
          type: string
          nullable: true
          format: uri
        url:
          type: string
          nullable: true
          format: uri
        participant_count:
          type: integer
        question_count:
          type: integer
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ResearchStudyListResponse:
      type: object
      properties:
        studies:
          type: array
          items:
            $ref: '#/components/schemas/ResearchStudyListItem'
        count:
          type: integer
        total:
          type: integer
    ResearchStudyDetailResponse:
      type: object
      properties:
        study:
          $ref: '#/components/schemas/ResearchStudyListItem'
    ResearchStudyDeleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        deleted_id:
          type: integer
        deleted_uuid:
          type: string
    ZeitgeistAnswerOptions:
      description: Answer options as a list of labels, nested lists, or a structured object.
      oneOf:
      - type: array
        items:
          type: string
      - type: array
        items:
          type: array
          items:
            type: string
      - type: object
      - type: string
    ZeitgeistResultPayload:
      description: Aggregated counts keyed by answer option, or a structured payload.
      oneOf:
      - type: object
        additionalProperties:
          type: integer
      - type: array
        items:
          type: object
      - type: string
    ZeitgeistSurveyCreateRequest:
      type: object
      required:
      - title
      - question
      - research_group_uuid
      properties:
        title:
          type: string
        question:
          type: string
        description:
          type: string
          nullable: true
        instructions:
          type: string
          nullable: true
        research_group_uuid:
          type: string
        sample_size:
          type: integer
        day_of_week:
          type: integer
          minimum: 1
          maximum: 7
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
        answer_options:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        answer_colour:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        enabled:
          type: boolean
    ZeitgeistSurvey:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: string
          nullable: true
        question:
          type: string
        instructions:
          type: string
          nullable: true
        research_group_id:
          type: integer
        research_group_uuid:
          type: string
        sample_size:
          type: integer
          nullable: true
        day_of_week:
          type: integer
          nullable: true
          minimum: 1
          maximum: 7
        day_of_month:
          type: integer
          nullable: true
          minimum: 1
          maximum: 31
        answer_options:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        answer_colour:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ZeitgeistSurveyResponse:
      type: object
      properties:
        survey:
          $ref: '#/components/schemas/ZeitgeistSurvey'
    ZeitgeistSurveyDeleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        deleted_id:
          type: integer
    ZeitgeistSurveyResult:
      type: object
      properties:
        id:
          type: integer
        survey_id:
          type: integer
        title:
          type: string
        period_date:
          type: string
          format: date
          nullable: true
        period_label:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        calculated_result:
          type: number
          nullable: true
        result:
          $ref: '#/components/schemas/ZeitgeistResultPayload'
    ZeitgeistSurveyResultListResponse:
      type: object
      properties:
        survey_id:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/ZeitgeistSurveyResult'
        count:
          type: integer
        total:
          type: integer
        page:
          type: integer
        page_size:
          type: integer
        order_by:
          type: string
        order:
          type: string
    AddGroupAgentsRequest:
      type: object
      required:
      - agent_ids
      properties:
        agent_ids:
          type: array
          items:
            type: integer
    AddGroupAgentsResponse:
      type: object
      properties:
        group_id:
          type: integer
        group_url:
          type: string
          nullable: true
          format: uri
        added_agent_ids:
          type: array
          items:
            type: integer
        added_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        added_count:
          type: integer
        already_member_agent_ids:
          type: array
          items:
            type: integer
        already_member_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        not_found_agent_ids:
          type: array
          items:
            type: integer
        invalid_agent_ids:
          type: array
          items:
            oneOf:
            - type: integer
            - type: string
            - type: boolean
            - type: number
            - type: object
              additionalProperties: true
            - type: array
              items: {}
        total_agent_count:
          type: integer
    RemoveGroupAgentsRequest:
      type: object
      required:
      - agent_ids
      properties:
        agent_ids:
          type: array
          items:
            type: integer
    RemoveGroupAgentsResponse:
      type: object
      properties:
        group_id:
          type: integer
        group_url:
          type: string
          nullable: true
          format: uri
        removed_agent_ids:
          type: array
          items:
            type: integer
        removed_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        removed_count:
          type: integer
    RemoveStudyAgentsRequest:
      type: object
      required:
      - agent_ids
      properties:
        agent_ids:
          type: array
          items:
            type: integer
    RemoveStudyAgentsResponse:
      type: object
      properties:
        study_id:
          type: integer
        removed_agent_ids:
          type: array
          items:
            type: integer
        removed_agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
        removed_count:
          type: integer
    StudyQuestionCreateRequest:
      type: object
      required:
      - question
      properties:
        question:
          type: string
        target_group_uuids:
          type: array
          items:
            type: string
        target_agent_uuids:
          type: array
          items:
            type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
      description: 'Questions are always broadcast. Provide `target_group_uuids` or `target_agent_uuids`.

        If omitted, the study''s attached groups are used. A request with no targets returns 400.

        Allowed attachment types: images (image/png, image/jpeg, image/gif, image/webp) and PDFs (application/pdf).

        For study questions, `media_asset_id` is recommended (upload first via `POST /v1/media-assets`).

        `data_url` and `file_data` (base64) are also supported.

        '
    StudyAnswer:
      type: object
      properties:
        id:
          type: integer
        study_id:
          type: integer
        question_id:
          type: integer
        submitted_at:
          type: string
          format: date-time
          nullable: true
        response_text:
          type: string
        agent_id:
          type: integer
          nullable: true
        agent_uuid:
          type: string
          nullable: true
        agent_name:
          type: string
          nullable: true
        agent_age:
          type: integer
          nullable: true
        agent_city:
          type: string
          nullable: true
        agent_state:
          type: string
          nullable: true
        agent_country:
          type: string
          nullable: true
        agent_location:
          type: string
          nullable: true
        agent_occupation:
          type: string
          nullable: true
        agent_summary:
          type: string
          nullable: true
        agent_elevenlabs_voice_id:
          type: string
          nullable: true
        agent_image:
          type: string
          nullable: true
        audio_file_id:
          type: string
          nullable: true
    StudyQuestionWithAnswers:
      type: object
      properties:
        id:
          type: integer
        study_id:
          type: integer
        question:
          type: string
        optimized_question:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        answer_count:
          type: integer
        answers:
          type: array
          items:
            $ref: '#/components/schemas/StudyAnswer'
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachment'
    StudyQuestionListResponse:
      type: object
      properties:
        study_id:
          type: integer
        study_title:
          type: string
        count:
          type: integer
        questions:
          type: array
          items:
            $ref: '#/components/schemas/StudyQuestionWithAnswers'
    StudyQuestionBroadcastResponse:
      type: object
      properties:
        status:
          type: string
          example: queued
        job_ids:
          type: array
          items:
            type: string
        study_id:
          type: integer
        question_id:
          type: integer
        agent_ids:
          type: array
          items:
            type: integer
        agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentLink'
      example:
        status: queued
        study_id: 101
        question_id: 555
        job_ids:
        - job_001
        - job_002
        agent_ids:
        - 2001
        - 2002
    StudyCompletionRequest:
      type: object
      properties:
        steps:
          type: array
          items:
            type: string
          description: 'Optional list of pipeline steps to run. Defaults to full pipeline.


            Common step identifiers:

            - study_correlations (correlations)

            - study_recommendations (insights and recommended actions)

            - study_research_summary (summary)

            - study_tldr (final takeaways)

            '
        force:
          type: boolean
          default: false
          description: Re-run steps that already completed.
    StudyCompletionResponse:
      type: object
      properties:
        job_ids:
          type: array
          items:
            type: string
          description: Background job IDs enqueued for the completion pipeline.
        queued_steps:
          type: array
          items:
            type: string
          description: Completion steps queued for execution (may be fewer if not forced).
    ShareUpdateRequest:
      type: object
      properties:
        enabled:
          type: boolean
        expires_at:
          type: string
          format: date-time
        shareable_until:
          type: string
          format: date-time
        regenerate:
          type: boolean
      example:
        enabled: true
        shareable_until: 2026-01-31 00:00:00+00:00
        regenerate: false
    ShareInfo:
      type: object
      properties:
        enabled:
          type: boolean
        url:
          type: string
          nullable: true
        token:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        is_expired:
          type: boolean
    JobStatus:
      type: object
      properties:
        status:
          type: string
        result:
          type: object
          nullable: true
        error:
          type: string
          nullable: true
        meta:
          type: object
      example:
        status: finished
        result: {}
        meta: {}
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
