openapi: 3.1.0
info:
  title: SurveyMint Public API
  version: 1.0.0-beta
  summary: Beta user, survey, response, exchange, and credit capabilities.
  description: |
    Public v1 contract for custom SurveyMint applications and user interfaces.
    IDs are opaque strings, timestamps are UTC RFC 3339 values, and credits are integers.
    Errors use application/problem+json. See docs/public-api.md for lifecycle and
    implementation requirements. The operations named in `x-surveymint-live-operations`
    are live in the beta API; the remaining operations are the forward compatibility
    roadmap and must not be treated as available yet.
  contact:
    name: SurveyMint
    url: https://surveymint.io
  license:
    name: Proprietary
    url: https://surveymint.io/terms
x-surveymint-stability: beta
x-surveymint-live-operations:
  - getCurrentUser
  - updateCurrentUser
  - getCreditBalance
  - getCreditStatistics
  - listCreditTransactions
  - listSurveys
  - createSurvey
  - getSurvey
  - updateSurvey
  - deleteSurvey
  - listSurveyQuestions
  - replaceSurveyQuestions
  - transitionSurvey
  - listSurveyResponses
  - getSurveyAnalytics
  - listExchangeSurveys
  - publishSurveyToExchange
  - getExchangeSurvey
  - cancelExchangeSurvey
  - resumeExchangeSurvey
  - listExchangeSurveyQuestions
  - submitExchangeSurveyResponse
  - getApiMetadata
servers:
  - url: https://surveymint.io/api/v1
    description: Production
tags:
  - name: Users
    description: Account registration, profile, preferences, verification, password, and deletion.
  - name: API keys
    description: User-generated, scoped API key lifecycle.
  - name: Credits
    description: Spendable balance, reservations, statistics, and immutable ledger entries.
  - name: Surveys
    description: Owned survey authoring and lifecycle management.
  - name: Survey responses
    description: Direct response submission and owner-authorized response access.
  - name: Exchange
    description: Funded survey listings, eligibility, rewarded responses, and participation history.
  - name: Billing
    description: Server-priced credit packages and hosted checkout sessions.
  - name: Analytics
    description: Survey-level and account-level performance summaries.
  - name: Metadata
    description: Public catalog values, capabilities, and API limits.
security:
  - oauth2: []
  - apiKey: []
paths:
  /users:
    post:
      tags: [Users]
      operationId: registerUser
      summary: Register a SurveyMint account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserRegistration'
      responses:
        '201':
          description: Account registered; email verification may still be required.
          headers:
            Location:
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEnvelope'
        '422': { $ref: '#/components/responses/ValidationProblem' }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /users/me:
    get:
      tags: [Users]
      operationId: getCurrentUser
      summary: Get the authenticated account and profile
      security:
        - oauth2: ["profile:read"]
        - apiKey: []
      responses:
        '200':
          description: Current user.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
    patch:
      tags: [Users]
      operationId: updateCurrentUser
      summary: Update profile and preferences
      security:
        - oauth2: ["profile:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdate'
      responses:
        '200':
          description: Updated user.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEnvelope'
        '412': { $ref: '#/components/responses/PreconditionProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
    delete:
      tags: [Users]
      operationId: requestCurrentUserDeletion
      summary: Request account deletion
      description: Requires a recent authentication challenge. Repeating the request is idempotent.
      security:
        - oauth2: ["account:write"]
      responses:
        '202':
          description: Deletion request recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeletionEnvelope'
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
  /users/me/deletion-cancellations:
    post:
      tags: [Users]
      operationId: cancelCurrentUserDeletion
      summary: Cancel a pending deletion request
      security:
        - oauth2: ["account:write"]
      responses:
        '200':
          description: Deletion cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
  /users/me/verification-emails:
    post:
      tags: [Users]
      operationId: resendVerificationEmail
      summary: Resend the current user's verification email
      security:
        - oauth2: ["account:write"]
      responses:
        '202': { description: Request accepted whether or not another email was needed. }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /users/me/password-changes:
    post:
      tags: [Users]
      operationId: changeCurrentUserPassword
      summary: Change the current user's password
      description: OAuth user context and a recent authentication challenge are required.
      security:
        - oauth2: ["account:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordChange'
      responses:
        '204': { description: Password changed and older sessions or refresh tokens revoked. }
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /users/me/api-keys:
    get:
      tags: [API keys]
      operationId: listApiKeys
      summary: List API key metadata
      security:
        - oauth2: ["account:write"]
      responses:
        '200':
          description: Token metadata; secret values are never returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
    post:
      tags: [API keys]
      operationId: createApiKey
      summary: Create an API key
      description: The secret is returned exactly once and requires recent authentication.
      security:
        - oauth2: ["account:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
      responses:
        '201':
          description: Token created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreatedEnvelope'
        '422': { $ref: '#/components/responses/ValidationProblem' }
  /users/me/api-keys/{key_id}:
    parameters:
      - $ref: '#/components/parameters/KeyId'
    delete:
      tags: [API keys]
      operationId: revokeApiKey
      summary: Revoke an API key
      security:
        - oauth2: ["account:write"]
      responses:
        '204': { description: "Revoked, or already revoked." }
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /users/me/credits:
    get:
      tags: [Credits]
      operationId: getCreditBalance
      summary: Get spendable and reserved credit balances
      security:
        - oauth2: ["credits:read"]
        - apiKey: []
      responses:
        '200':
          description: Credit summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditSummaryEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
  /users/me/credit-statistics:
    get:
      tags: [Credits]
      operationId: getCreditStatistics
      summary: Get aggregate credit statistics
      security:
        - oauth2: ["credits:read"]
        - apiKey: []
      responses:
        '200':
          description: Credit statistics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditStatisticsEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
  /users/me/credit-transactions:
    get:
      tags: [Credits]
      operationId: listCreditTransactions
      summary: List immutable credit ledger entries
      security:
        - oauth2: ["credits:read"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: type
          in: query
          schema:
            $ref: '#/components/schemas/CreditTransactionType'
      responses:
        '200':
          description: Newest entries first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditTransactionListEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
  /surveys:
    get:
      tags: [Surveys]
      operationId: listSurveys
      summary: List surveys owned by the current user
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/SurveyStatus' }
        - name: category
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Newest surveys first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyListEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
    post:
      tags: [Surveys]
      operationId: createSurvey
      summary: Create a draft survey and its initial questions
      security:
        - oauth2: ["surveys:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurveyCreate'
      responses:
        '201':
          description: Survey created.
          headers:
            Location: { schema: { type: string } }
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyEnvelope'
        '422': { $ref: '#/components/responses/ValidationProblem' }
  /surveys/{survey_id}:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Surveys]
      operationId: getSurvey
      summary: Get an owned survey
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      responses:
        '200':
          description: Survey with questions.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
    patch:
      tags: [Surveys]
      operationId: updateSurvey
      summary: Update survey metadata and settings
      security:
        - oauth2: ["surveys:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurveyUpdate'
      responses:
        '200':
          description: Updated survey.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '412': { $ref: '#/components/responses/PreconditionProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
    delete:
      tags: [Surveys]
      operationId: deleteSurvey
      summary: Delete an owned survey
      security:
        - oauth2: ["surveys:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      responses:
        '204': { description: Survey deleted. }
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '412': { $ref: '#/components/responses/PreconditionProblem' }
  /surveys/{survey_id}/questions:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Surveys]
      operationId: listSurveyQuestions
      summary: List an owned survey's ordered questions
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      responses:
        '200':
          description: Ordered question set.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuestionListEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
    put:
      tags: [Surveys]
      operationId: replaceSurveyQuestions
      summary: Atomically replace an owned survey's ordered questions
      security:
        - oauth2: ["surveys:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [questions]
              properties:
                questions:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { $ref: '#/components/schemas/QuestionInput' }
      responses:
        '200':
          description: Replaced question set.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuestionListEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '412': { $ref: '#/components/responses/PreconditionProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
  /surveys/{survey_id}/transitions:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    post:
      tags: [Surveys]
      operationId: transitionSurvey
      summary: Publish, pause, resume, or close a survey
      security:
        - oauth2: ["surveys:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurveyTransition'
      responses:
        '200':
          description: Transition applied or already in the requested state.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '412': { $ref: '#/components/responses/PreconditionProblem' }
  /surveys/{survey_id}/responses:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Survey responses]
      operationId: listSurveyResponses
      summary: List response summaries for an owned survey
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Newest responses first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseListEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /surveys/{survey_id}/responses/{response_id}:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
      - $ref: '#/components/parameters/ResponseId'
    get:
      tags: [Survey responses]
      operationId: getSurveyResponse
      summary: Get one response and its answers
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      responses:
        '200':
          description: Response details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /surveys/{survey_id}/analytics:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Analytics]
      operationId: getSurveyAnalytics
      summary: Get owner-only survey analytics
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      responses:
        '200':
          description: Survey analytics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurveyAnalyticsEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /surveys/{survey_id}/responses.csv:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Survey responses]
      operationId: exportSurveyResponsesCsv
      summary: Stream owner-authorized survey responses as CSV
      security:
        - oauth2: ["surveys:read"]
        - apiKey: []
      responses:
        '200':
          description: UTF-8 CSV with a header row.
          content:
            text/csv:
              schema: { type: string, contentMediaType: text/csv }
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /public/surveys/{survey_id}:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Survey responses]
      operationId: getPublicSurvey
      summary: Get safe presentation metadata for an active public survey
      security: []
      responses:
        '200':
          description: Public survey presentation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSurveyEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /public/surveys/{survey_id}/questions:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    get:
      tags: [Survey responses]
      operationId: listPublicSurveyQuestions
      summary: Get answerable questions for an active public survey
      security: []
      responses:
        '200':
          description: Presentation-safe ordered questions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuestionListEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /public/surveys/{survey_id}/responses:
    parameters:
      - $ref: '#/components/parameters/SurveyId'
    post:
      tags: [Survey responses]
      operationId: submitPublicSurveyResponse
      summary: Submit a complete direct response
      description: Authentication is optional only if the survey allows anonymous responses.
      security:
        - {}
        - oauth2: ["responses:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResponseSubmission'
      responses:
        '201':
          description: Immutable response created.
          headers:
            Location: { schema: { type: string } }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseReceiptEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /exchange/surveys:
    get:
      tags: [Exchange]
      operationId: listExchangeSurveys
      summary: List available exchange listings or the current user's owned listings
      security:
        - oauth2: ["exchange:read"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: view
          in: query
          description: Available listings are eligibility-aware; owned listings include every status.
          schema: { type: string, enum: [available, owned], default: available }
        - name: category
          in: query
          schema: { type: string }
        - name: min_reward
          in: query
          schema: { type: integer, minimum: 0 }
        - name: max_minutes
          in: query
          schema: { type: integer, minimum: 1 }
        - name: include_ineligible
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Eligibility-aware exchange listing page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeListEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
    post:
      tags: [Exchange]
      operationId: publishSurveyToExchange
      summary: Fund and publish an owned active survey to the exchange
      security:
        - oauth2: ["exchange:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeCreate'
      responses:
        '201':
          description: Listing created and exact budget reserved.
          headers:
            Location: { schema: { type: string } }
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeCreatedEnvelope'
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
  /exchange/surveys/{exchange_id}:
    parameters:
      - $ref: '#/components/parameters/ExchangeId'
    get:
      tags: [Exchange]
      operationId: getExchangeSurvey
      summary: Get an eligible or owned exchange listing
      security:
        - oauth2: ["exchange:read"]
        - apiKey: []
      responses:
        '200':
          description: Exchange listing.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeEnvelope'
        '404': { $ref: '#/components/responses/NotFoundProblem' }
    patch:
      tags: [Exchange]
      operationId: updateExchangeSurvey
      summary: Update mutable metadata on an owned listing
      security:
        - oauth2: ["exchange:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeUpdate'
      responses:
        '200':
          description: Updated listing.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeEnvelope'
        '412': { $ref: '#/components/responses/PreconditionProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
    delete:
      tags: [Exchange]
      operationId: cancelExchangeSurvey
      summary: Cancel a non-terminal owned listing and refund unused reserved credits
      security:
        - oauth2: ["exchange:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/IfMatch'
      responses:
        '200':
          description: Cancelled listing and post-refund credit summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeCancelledEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '412': { $ref: '#/components/responses/PreconditionProblem' }
  /exchange/surveys/{exchange_id}/resumptions:
    parameters:
      - $ref: '#/components/parameters/ExchangeId'
    post:
      tags: [Exchange]
      operationId: resumeExchangeSurvey
      summary: Resume an owned paused listing after complete revalidation
      security:
        - oauth2: ["exchange:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/IfMatch'
      responses:
        '200':
          description: Resumed listing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeEnvelope'
        '409': { $ref: '#/components/responses/ConflictProblem' }
  /exchange/surveys/{exchange_id}/questions:
    parameters:
      - $ref: '#/components/parameters/ExchangeId'
    get:
      tags: [Exchange]
      operationId: listExchangeSurveyQuestions
      summary: Get presentation-safe questions for an eligible listing
      security:
        - oauth2: ["exchange:read"]
        - apiKey: []
      responses:
        '200':
          description: Ordered questions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuestionListEnvelope'
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
        '404': { $ref: '#/components/responses/NotFoundProblem' }
  /exchange/surveys/{exchange_id}/responses:
    parameters:
      - $ref: '#/components/parameters/ExchangeId'
    post:
      tags: [Exchange]
      operationId: submitExchangeSurveyResponse
      summary: Submit answers and atomically earn the advertised credit reward
      security:
        - oauth2: ["responses:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResponseSubmission'
      responses:
        '201':
          description: Response, reward, and post-transaction balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RewardedResponseReceiptEnvelope'
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
        '409': { $ref: '#/components/responses/ConflictProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /exchange/participations:
    get:
      tags: [Exchange]
      operationId: listExchangeParticipations
      summary: List the current user's exchange participation history
      security:
        - oauth2: ["exchange:read"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Newest participation first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipationListEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
  /billing/packages:
    get:
      tags: [Billing]
      operationId: listBillingPackages
      summary: List current server-priced credit packages
      security: []
      responses:
        '200':
          description: Purchasable packages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingPackageListEnvelope'
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /billing/checkout-sessions:
    post:
      tags: [Billing]
      operationId: createBillingCheckoutSession
      summary: Create a hosted Stripe checkout session
      security:
        - oauth2: ["billing:write"]
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutSessionCreate'
      responses:
        '201':
          description: Hosted checkout session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutSessionEnvelope'
        '403': { $ref: '#/components/responses/AuthorizationProblem' }
        '422': { $ref: '#/components/responses/ValidationProblem' }
        '429': { $ref: '#/components/responses/RateLimitProblem' }
  /analytics/overview:
    get:
      tags: [Analytics]
      operationId: getAnalyticsOverview
      summary: Get account-level dashboard analytics
      security:
        - oauth2: ["surveys:read", "credits:read"]
        - apiKey: []
      responses:
        '200':
          description: Aggregate account analytics.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsOverviewEnvelope'
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
  /meta:
    get:
      tags: [Metadata]
      operationId: getApiMetadata
      summary: Get supported enums, catalog values, and limits
      security: []
      responses:
        '200':
          description: Public API capabilities.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetadataEnvelope'
        '429': { $ref: '#/components/responses/RateLimitProblem' }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: Authorization Code with PKCE for user-facing applications.
      flows:
        authorizationCode:
          authorizationUrl: https://surveymint.io/oauth/authorize
          tokenUrl: https://surveymint.io/oauth/token
          refreshUrl: https://surveymint.io/oauth/token
          scopes:
            profile:read: Read account and profile.
            profile:write: Update profile and preferences.
            account:write: Perform sensitive account actions after recent authentication.
            credits:read: Read credit balances and ledger entries.
            surveys:read: Read owned surveys, responses, analytics, and exports.
            surveys:write: Create and modify owned surveys.
            exchange:read: Browse exchange listings and participation history.
            exchange:write: Fund and manage exchange listings.
            responses:write: Submit survey responses.
            billing:write: Create checkout sessions.
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Scoped `sm_live_...` API key generated by the user in SurveyMint's API Keys UI.
  parameters:
    SurveyId:
      name: survey_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Id' }
    ExchangeId:
      name: exchange_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Id' }
    ResponseId:
      name: response_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Id' }
    KeyId:
      name: key_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Id' }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string, maxLength: 500 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 16, maxLength: 255 }
    IfMatch:
      name: If-Match
      in: header
      required: false
      description: Strong ETag returned by the latest resource read.
      schema: { type: string }
  headers:
    ETag:
      description: Strong resource version for optimistic concurrency.
      schema: { type: string }
  responses:
    AuthenticationProblem:
      description: Authentication is required or the bearer token is invalid.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    AuthorizationProblem:
      description: The subject lacks scope, recent authentication, or eligibility.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotFoundProblem:
      description: Resource was not found or is hidden by ownership rules.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    ValidationProblem:
      description: Request fields or answers are invalid.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ValidationProblem' }
    ConflictProblem:
      description: Domain state conflicts with the requested operation.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    PreconditionProblem:
      description: The supplied ETag is stale.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    RateLimitProblem:
      description: Too many requests.
      headers:
        Retry-After:
          schema: { type: integer, minimum: 1 }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
  schemas:
    Id:
      type: string
      minLength: 1
      maxLength: 64
      examples: [01K4T9F5P7QH3JJKS6ZX9XR4GN]
    Timestamp:
      type: string
      format: date-time
    Page:
      type: object
      required: [next_cursor, has_more]
      properties:
        next_cursor: { type: [string, 'null'] }
        has_more: { type: boolean }
    Problem:
      type: object
      required: [type, title, status, detail, code, request_id]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer, minimum: 400, maximum: 599 }
        detail: { type: string }
        code: { type: string, pattern: '^[a-z0-9_]+$' }
        request_id: { type: string }
    ValidationProblem:
      allOf:
        - $ref: '#/components/schemas/Problem'
        - type: object
          required: [errors]
          properties:
            errors:
              type: array
              items:
                type: object
                required: [pointer, message]
                properties:
                  pointer: { type: string, description: JSON Pointer into the request. }
                  message: { type: string }
                  code: { type: string }
    UserRegistration:
      type: object
      additionalProperties: false
      required: [email, password, terms_accepted]
      properties:
        email: { type: string, format: email, maxLength: 254 }
        password: { type: string, format: password, minLength: 12, maxLength: 128, writeOnly: true }
        first_name: { type: string, minLength: 1, maxLength: 50 }
        last_name: { type: string, minLength: 1, maxLength: 50 }
        terms_accepted: { type: boolean, const: true }
    UserUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        first_name: { type: string, minLength: 1, maxLength: 50 }
        last_name: { type: string, minLength: 1, maxLength: 50 }
        avatar: { type: [string, 'null'], maxLength: 200000 }
        profile: { $ref: '#/components/schemas/UserProfileInput' }
        preferences: { $ref: '#/components/schemas/UserPreferencesInput' }
    UserProfileInput:
      type: object
      additionalProperties: false
      properties:
        institution: { type: string, minLength: 1, maxLength: 100 }
        current_occupation: { type: string, enum: [undergraduate, graduate, phd, postdoc, faculty, researcher, professional, other] }
        country: { type: string, pattern: '^[a-z]{2}$' }
        field_of_study: { type: string, minLength: 1, maxLength: 120 }
        academic_level: { type: string, enum: [undergraduate, masters, phd, post_doc, professor, researcher] }
        years_of_experience: { type: integer, minimum: 0, maximum: 50 }
        bio: { type: string, maxLength: 500 }
        research_interests: { type: string, maxLength: 1000 }
        date_of_birth: { type: [string, 'null'], format: date }
        gender: { type: [string, 'null'], maxLength: 50 }
        languages: { type: array, uniqueItems: true, items: { type: string } }
    UserPreferencesInput:
      type: object
      additionalProperties: false
      properties:
        interested_categories: { type: array, uniqueItems: true, items: { type: string } }
        preferred_survey_length: { type: [string, 'null'], enum: [SHORT, MEDIUM, LONG, null] }
        weekly_availability: { type: string, enum: [LOW, MEDIUM, HIGH] }
        notifications: { type: boolean }
        email_notifications: { type: boolean }
        reminder_notifications: { type: boolean }
    User:
      type: object
      required: [id, email, email_verified, onboarded, credits, profile, preferences, statistics, created_at, updated_at]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        email: { type: string, format: email }
        first_name: { type: [string, 'null'] }
        last_name: { type: [string, 'null'] }
        avatar: { type: [string, 'null'] }
        email_verified: { type: boolean }
        onboarded: { type: boolean }
        credits: { type: integer, minimum: 0 }
        billing_restricted: { type: boolean }
        deletion_requested_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
        deletion_scheduled_for: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
        profile: { $ref: '#/components/schemas/UserProfileInput' }
        preferences: { $ref: '#/components/schemas/UserPreferencesInput' }
        statistics:
          type: object
          required: [total_surveys, total_responses, total_credits_earned, total_credits_spent]
          properties:
            total_surveys: { type: integer, minimum: 0 }
            total_responses: { type: integer, minimum: 0 }
            total_credits_earned: { type: integer, minimum: 0 }
            total_credits_spent: { type: integer, minimum: 0 }
        created_at: { $ref: '#/components/schemas/Timestamp' }
        updated_at: { $ref: '#/components/schemas/Timestamp' }
    UserEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/User' } }
    DeletionEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [status, deletion_scheduled_for]
          properties:
            status: { type: string, const: pending }
            deletion_scheduled_for: { $ref: '#/components/schemas/Timestamp' }
    PasswordChange:
      type: object
      additionalProperties: false
      required: [current_password, new_password]
      properties:
        current_password: { type: string, format: password, writeOnly: true }
        new_password: { type: string, format: password, minLength: 12, maxLength: 128, writeOnly: true }
    ApiScope:
      type: string
      enum: ["profile:read", "profile:write", "credits:read", "surveys:read", "surveys:write", "exchange:read", "exchange:write", "responses:write", "billing:write"]
    ApiKeyCreate:
      type: object
      additionalProperties: false
      required: [name, scopes]
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        scopes: { type: array, minItems: 1, uniqueItems: true, items: { $ref: '#/components/schemas/ApiScope' } }
        expires_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
    ApiKey:
      type: object
      required: [id, name, prefix, scopes, created_at]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        name: { type: string }
        prefix: { type: string }
        scopes: { type: array, items: { $ref: '#/components/schemas/ApiScope' } }
        expires_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
        last_used_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
        revoked_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
        created_at: { $ref: '#/components/schemas/Timestamp' }
    ApiKeyListEnvelope:
      type: object
      required: [data]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/ApiKey' } } }
    ApiKeyCreatedEnvelope:
      type: object
      required: [data]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/ApiKey'
            - type: object
              required: [secret]
              properties:
                secret:
                  type: string
                  description: Returned only in this creation response and never retrievable again.
    CreditSummary:
      type: object
      required: [spendable, reserved, total]
      properties:
        spendable: { type: integer, minimum: 0 }
        reserved: { type: integer, minimum: 0 }
        total: { type: integer, minimum: 0 }
    CreditSummaryEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/CreditSummary' } }
    CreditStatistics:
      type: object
      required: [total_earned, total_spent, current_balance, transaction_count]
      properties:
        total_earned: { type: integer, minimum: 0 }
        total_spent: { type: integer, minimum: 0 }
        current_balance: { type: integer, minimum: 0 }
        transaction_count: { type: integer, minimum: 0 }
    CreditStatisticsEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/CreditStatistics' } }
    CreditTransactionType:
      type: string
      enum: [earned, spent, bonus, refund, admin_grant, admin_deduct, referral, purchase, expired, transfer_in, transfer_out]
    CreditTransaction:
      type: object
      required: [id, type, status, amount, balance, description, related, created_at]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        type: { $ref: '#/components/schemas/CreditTransactionType' }
        status: { type: string, enum: [pending, completed, failed, cancelled] }
        amount: { type: integer }
        balance: { type: integer, minimum: 0 }
        description: { type: string }
        reason: { type: [string, 'null'] }
        related:
          type: object
          properties:
            survey_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
            exchange_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
            response_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
            order_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
        created_at: { $ref: '#/components/schemas/Timestamp' }
    CreditTransactionListEnvelope:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/CreditTransaction' } }
        page: { $ref: '#/components/schemas/Page' }
    SurveyStatus:
      type: string
      enum: [draft, active, paused, closed]
    SurveySettings:
      type: object
      properties:
        allow_anonymous: { type: boolean, default: false }
        randomize_questions: { type: boolean, default: false }
        show_progress_bar: { type: boolean, default: true }
        allow_back_navigation: { type: boolean, default: true }
        require_all_questions: { type: boolean, default: false }
        max_responses_per_user: { type: integer, minimum: 1, default: 1 }
        target_criteria: { $ref: '#/components/schemas/TargetCriteria' }
    TargetCriteria:
      type: object
      properties:
        countries: { type: array, uniqueItems: true, items: { type: string, pattern: '^[a-z]{2}$' } }
        academic_levels: { type: array, uniqueItems: true, items: { type: string } }
        fields_of_study: { type: array, uniqueItems: true, items: { type: string } }
        min_age: { type: integer, minimum: 18, maximum: 120 }
        max_age: { type: integer, minimum: 18, maximum: 120 }
        languages: { type: array, uniqueItems: true, items: { type: string } }
        genders: { type: array, uniqueItems: true, items: { type: string } }
        interested_categories: { type: array, uniqueItems: true, items: { type: string } }
    QuestionType:
      type: string
      enum: [single_choice, multiple_choice, text, rating, scale, date]
    QuestionOptionInput:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        text: { type: string, minLength: 1, maxLength: 500 }
    QuestionValidation:
      type: object
      additionalProperties: false
      properties:
        min_length: { type: integer, minimum: 0 }
        max_length: { type: integer, minimum: 0 }
        min: { type: number }
        max: { type: number }
    QuestionInput:
      type: object
      additionalProperties: false
      required: [text, type]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        text: { type: string, minLength: 1, maxLength: 2000 }
        type: { $ref: '#/components/schemas/QuestionType' }
        description: { type: [string, 'null'], maxLength: 2000 }
        placeholder: { type: [string, 'null'], maxLength: 500 }
        required: { type: boolean, default: false }
        include_other: { type: boolean, default: false }
        options:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/QuestionOptionInput' }
        validation: { $ref: '#/components/schemas/QuestionValidation' }
    Question:
      allOf:
        - $ref: '#/components/schemas/QuestionInput'
        - type: object
          required: [id, order_index, created_at, updated_at]
          properties:
            id: { $ref: '#/components/schemas/Id' }
            order_index: { type: integer, minimum: 0 }
            created_at: { $ref: '#/components/schemas/Timestamp' }
            updated_at: { $ref: '#/components/schemas/Timestamp' }
    SurveyCreate:
      type: object
      additionalProperties: false
      required: [title, questions]
      properties:
        title: { type: string, minLength: 1, maxLength: 200 }
        description: { type: [string, 'null'], maxLength: 5000 }
        category: { type: [string, 'null'] }
        estimated_minutes: { type: [integer, 'null'], minimum: 1, maximum: 1440 }
        target_responses: { type: integer, minimum: 1, default: 100 }
        settings: { $ref: '#/components/schemas/SurveySettings' }
        questions:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: '#/components/schemas/QuestionInput' }
    SurveyUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title: { type: string, minLength: 1, maxLength: 200 }
        description: { type: [string, 'null'], maxLength: 5000 }
        category: { type: [string, 'null'] }
        estimated_minutes: { type: [integer, 'null'], minimum: 1, maximum: 1440 }
        target_responses: { type: integer, minimum: 1 }
        settings: { $ref: '#/components/schemas/SurveySettings' }
    Survey:
      allOf:
        - $ref: '#/components/schemas/SurveyUpdate'
        - type: object
          required: [id, status, title, target_responses, settings, statistics, questions, created_at, updated_at]
          properties:
            id: { $ref: '#/components/schemas/Id' }
            status: { $ref: '#/components/schemas/SurveyStatus' }
            response_count: { type: integer, minimum: 0 }
            statistics:
              type: object
              required: [total_responses, completion_rate, average_completion_seconds]
              properties:
                total_responses: { type: integer, minimum: 0 }
                completion_rate: { type: number, minimum: 0, maximum: 100 }
                average_completion_seconds: { type: integer, minimum: 0 }
                last_response_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
            questions: { type: array, items: { $ref: '#/components/schemas/Question' } }
            created_at: { $ref: '#/components/schemas/Timestamp' }
            updated_at: { $ref: '#/components/schemas/Timestamp' }
    SurveyTransition:
      type: object
      additionalProperties: false
      required: [action]
      properties:
        action: { type: string, enum: [publish, pause, resume, close] }
    SurveyEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/Survey' } }
    SurveyListEnvelope:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Survey' } }
        page: { $ref: '#/components/schemas/Page' }
    QuestionListEnvelope:
      type: object
      required: [data]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/Question' } } }
    PublicSurvey:
      type: object
      required: [id, title, status, estimated_minutes, settings, owner]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        title: { type: string }
        description: { type: [string, 'null'] }
        status: { type: string, const: active }
        category: { type: [string, 'null'] }
        estimated_minutes: { type: [integer, 'null'] }
        settings:
          type: object
          required: [allow_anonymous, show_progress_bar, allow_back_navigation, require_all_questions]
          properties:
            allow_anonymous: { type: boolean }
            show_progress_bar: { type: boolean }
            allow_back_navigation: { type: boolean }
            require_all_questions: { type: boolean }
        owner:
          type: object
          properties:
            display_name: { type: [string, 'null'] }
    PublicSurveyEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/PublicSurvey' } }
    AnswerSubmission:
      type: object
      additionalProperties: false
      required: [question_id, value]
      properties:
        question_id: { $ref: '#/components/schemas/Id' }
        value:
          description: String for scalar questions or a string array for multiple choice.
          oneOf:
            - type: string
            - type: array
              uniqueItems: true
              items: { type: string }
            - type: 'null'
        skipped: { type: boolean, default: false }
    ResponseSubmission:
      type: object
      additionalProperties: false
      required: [answers, time_spent_seconds]
      properties:
        answers:
          type: array
          maxItems: 100
          items: { $ref: '#/components/schemas/AnswerSubmission' }
        time_spent_seconds: { type: integer, minimum: 0, maximum: 86400 }
    Answer:
      allOf:
        - $ref: '#/components/schemas/AnswerSubmission'
        - type: object
          required: [id]
          properties: { id: { $ref: '#/components/schemas/Id' } }
    Response:
      type: object
      required: [id, survey_id, started_at, completed_at, time_spent_seconds, is_complete, answers]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        survey_id: { $ref: '#/components/schemas/Id' }
        exchange_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
        respondent_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }] }
        started_at: { $ref: '#/components/schemas/Timestamp' }
        completed_at: { $ref: '#/components/schemas/Timestamp' }
        time_spent_seconds: { type: integer, minimum: 0 }
        is_complete: { type: boolean, const: true }
        answers: { type: array, items: { $ref: '#/components/schemas/Answer' } }
    ResponseReceipt:
      type: object
      required: [response_id, survey_id, completed_at]
      properties:
        response_id: { $ref: '#/components/schemas/Id' }
        survey_id: { $ref: '#/components/schemas/Id' }
        completed_at: { $ref: '#/components/schemas/Timestamp' }
    ResponseReceiptEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/ResponseReceipt' } }
    ResponseEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/Response' } }
    ResponseListEnvelope:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Response' } }
        page: { $ref: '#/components/schemas/Page' }
    SurveyAnalyticsEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [survey_id, total_responses, completed_responses, average_completion_seconds, questions]
          properties:
            survey_id: { $ref: '#/components/schemas/Id' }
            total_responses: { type: integer, minimum: 0 }
            completed_responses: { type: integer, minimum: 0 }
            average_completion_seconds: { type: integer, minimum: 0 }
            questions:
              type: array
              items:
                type: object
                required: [question_id, total_answers, skipped_count]
                properties:
                  question_id: { $ref: '#/components/schemas/Id' }
                  total_answers: { type: integer, minimum: 0 }
                  skipped_count: { type: integer, minimum: 0 }
    ExchangeStatus:
      type: string
      enum: [pending, active, paused, completed, cancelled]
    ExchangeCreate:
      type: object
      additionalProperties: false
      required: [survey_id, credits_per_response, target_responses]
      properties:
        survey_id: { $ref: '#/components/schemas/Id' }
        credits_per_response: { type: integer, minimum: 1 }
        target_responses: { type: integer, minimum: 1 }
        description: { type: [string, 'null'], maxLength: 5000 }
    ExchangeUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        description: { type: [string, 'null'], maxLength: 5000 }
        category: { type: [string, 'null'] }
        tags: { type: array, maxItems: 20, uniqueItems: true, items: { type: string, maxLength: 80 } }
        priority: { type: string, enum: [low, normal, high, urgent] }
        expires_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
    Eligibility:
      type: object
      required: [eligible]
      properties:
        eligible: { type: boolean }
        code: { type: [string, 'null'] }
    Exchange:
      allOf:
        - $ref: '#/components/schemas/ExchangeUpdate'
        - type: object
          required: [id, survey_id, status, credits_per_response, total_credits_offered, remaining_credits, target_responses, current_responses, eligibility, created_at, updated_at]
          properties:
            id: { $ref: '#/components/schemas/Id' }
            survey_id: { $ref: '#/components/schemas/Id' }
            status: { $ref: '#/components/schemas/ExchangeStatus' }
            credits_per_response: { type: integer, minimum: 1 }
            total_credits_offered: { type: integer, minimum: 0 }
            remaining_credits: { type: integer, minimum: 0 }
            target_responses: { type: integer, minimum: 1 }
            current_responses: { type: integer, minimum: 0 }
            estimated_minutes: { type: [integer, 'null'] }
            survey_title: { type: string }
            eligibility: { $ref: '#/components/schemas/Eligibility' }
            created_at: { $ref: '#/components/schemas/Timestamp' }
            updated_at: { $ref: '#/components/schemas/Timestamp' }
    ExchangeEnvelope:
      type: object
      required: [data]
      properties: { data: { $ref: '#/components/schemas/Exchange' } }
    ExchangeListEnvelope:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Exchange' } }
        page: { $ref: '#/components/schemas/Page' }
    ExchangeCreatedEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [exchange, credits]
          properties:
            exchange: { $ref: '#/components/schemas/Exchange' }
            credits: { $ref: '#/components/schemas/CreditSummary' }
    ExchangeCancelledEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [exchange, refunded_credits, credits]
          properties:
            exchange: { $ref: '#/components/schemas/Exchange' }
            refunded_credits: { type: integer, minimum: 0 }
            credits: { $ref: '#/components/schemas/CreditSummary' }
    RewardedResponseReceiptEnvelope:
      type: object
      required: [data]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/ResponseReceipt'
            - type: object
              required: [exchange_id, credits_earned, credit_balance]
              properties:
                exchange_id: { $ref: '#/components/schemas/Id' }
                credits_earned: { type: integer, minimum: 1 }
                credit_balance: { type: integer, minimum: 0 }
    Participation:
      type: object
      required: [id, exchange_id, survey_title, status, credits_earned, time_spent_seconds, joined_at]
      properties:
        id: { $ref: '#/components/schemas/Id' }
        exchange_id: { $ref: '#/components/schemas/Id' }
        survey_title: { type: string }
        survey_category: { type: [string, 'null'] }
        status: { type: string, enum: [joined, in_progress, completed, disqualified, cancelled] }
        credits_earned: { type: integer, minimum: 0 }
        time_spent_seconds: { type: integer, minimum: 0 }
        joined_at: { $ref: '#/components/schemas/Timestamp' }
        completed_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
    ParticipationListEnvelope:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Participation' } }
        page: { $ref: '#/components/schemas/Page' }
    BillingPackage:
      type: object
      required: [id, name, credits, amount_minor, currency, pricing_version]
      properties:
        id: { type: string }
        name: { type: string }
        credits: { type: integer, minimum: 1 }
        amount_minor: { type: integer, minimum: 0 }
        currency: { type: string, pattern: '^[a-z]{3}$' }
        pricing_version: { type: string }
    BillingPackageListEnvelope:
      type: object
      required: [data]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/BillingPackage' } } }
    CheckoutSessionCreate:
      type: object
      additionalProperties: false
      required: [package_id]
      properties:
        package_id: { type: string }
        credit_count: { type: integer, minimum: 1 }
    CheckoutSessionEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, url, expires_at]
          properties:
            id: { type: string }
            url: { type: string, format: uri }
            expires_at: { $ref: '#/components/schemas/Timestamp' }
    AnalyticsOverviewEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [surveys, responses, credits]
          properties:
            surveys:
              type: object
              properties:
                total: { type: integer, minimum: 0 }
                active: { type: integer, minimum: 0 }
            responses:
              type: object
              properties:
                received: { type: integer, minimum: 0 }
                submitted: { type: integer, minimum: 0 }
                average_completion_seconds: { type: integer, minimum: 0 }
            credits: { $ref: '#/components/schemas/CreditStatistics' }
    MetadataEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [api_version, question_types, survey_statuses, exchange_statuses, categories, countries, limits]
          properties:
            api_version: { type: string, const: v1 }
            question_types: { type: array, items: { $ref: '#/components/schemas/QuestionType' } }
            survey_statuses: { type: array, items: { $ref: '#/components/schemas/SurveyStatus' } }
            exchange_statuses: { type: array, items: { $ref: '#/components/schemas/ExchangeStatus' } }
            categories: { type: array, items: { type: string } }
            countries:
              type: array
              items:
                type: object
                required: [code, name]
                properties:
                  code: { type: string, pattern: '^[a-z]{2}$' }
                  name: { type: string }
            limits:
              type: object
              additionalProperties: { type: integer, minimum: 0 }
