openapi: 3.0.3
info:
  title: B5.ly Partner API (Headless)
  version: 2025-12
  description: |
    Headless partner API for advanced resellers who want to:

    - Render the Big Five assessment in their own UI (web/mobile).
    - Store answers and generate a paid 33-metrics report on-demand.
    - Consume credits (1 credit per paid report); credits are topped-up manually by B5.ly ops.

    Two API namespaces are used:
    - `v1` assessment endpoints: create a test session, fetch questions, submit answers (returns `resultId`).
    - `b5plus/v2` partner endpoints: credits, catalog, report creation and report retrieval (JSON only).

    Partner guide: `/developer/b5plus-partner-headless-guide.md`

    Supported language/locale values: `en`, `ar`, `es`.

servers:
  - url: "{baseUrl}"
    variables:
      baseUrl:
        default: "http://localhost:3000"
        description: Base URL for your integration environment.

tags:
  - name: Assessment (v1)
    description: Big Five assessment session + questions + answer submission.
  - name: Partner (v2)
    description: Partner auth + credits + report generation/retrieval.
  - name: Catalog (v2)
    description: Bundles and metric catalog metadata.
  - name: Reports (v2)
    description: Create reports and retrieve computed metrics (JSON only).
  - name: Customers (v2)
    description: Customer grants and customer-scoped report listings.
  - name: View Links (v2)
    description: Optional signed JSON links for sharing a report payload.

security:
  - bearerAuth: []

paths:
  /api/v1/tests/start:
    post:
      tags: [Assessment (v1)]
      summary: Start an assessment session
      description: |
        Creates an ephemeral test session (TTL ~14 days) and returns a `testSessionId`.

        Use this ID to fetch questions (`/items`) and submit answers (`/submit`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/V1StartRequest'
      responses:
        '200':
          description: Session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1StartResponse'
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '401':
          description: Missing/invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: APIKEY

  parameters:
    XProviderHeader:
      name: X-Provider
      in: header
      required: true
      description: Provider/reseller ID (must match the API key owner).
      schema:
        type: string
        minLength: 1
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique key for safe retries (24h reuse window).
      schema:
        type: string
        minLength: 1

  schemas:
    V1Error:
      type: object
      properties:
        code: { type: string }
        message: { type: string }
      required: [code, message]

    V1StartRequest:
      type: object
      properties:
        partnerUserRef:
          type: string
          description: Partner-owned stable customer identifier (optional).
        lang:
          type: string
          enum: [en, ar, es]
        meta:
          type: object
          properties:
            age:
              type: integer
              minimum: 10
              maximum: 100
            gender:
              type: string
              enum: [M, F]
            country:
              type: string
              description: ISO-3166 alpha-2 country code (e.g. US, SA).
              pattern: '^[A-Z]{2}$'
          required: [age, gender, country]
      required: [lang, meta]

    V1StartResponse:
      type: object
      properties:
        testSessionId:
          type: string
          pattern: '^[0-9a-fA-F]{24}$'
      required: [testSessionId]

    V1TestItem:
      type: object
      properties:
        questionID: { type: string }
        text: { type: string }
        domain:
          type: string
          enum: [A, C, E, N, O]
        facet:
          type: integer
          minimum: 1
          maximum: 6
      required: [questionID, text, domain, facet]

    V1ItemsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/V1TestItem'
      required: [items]

    V1SubmitRequest:
      type: object
      properties:
        testSessionId:
          type: string
          pattern: '^[0-9a-fA-F]{24}$'
        lang:
          type: string
          enum: [en, ar, es]
        finalize:
          type: boolean
          default: false
        answers:
          type: array
          description: |
            Answer objects must include `questionID`, `score`, `domain`, and `facet`.
            `domain`/`facet` come from `/api/v1/tests/items`.
          minItems: 1
          items:
            type: object
            properties:
              questionID: { type: string }
              score:
                type: integer
                minimum: 1
                maximum: 5
              domain:
                type: string
                enum: [A, C, E, N, O]
              facet:
                type: integer
                minimum: 1
                maximum: 6
            required: [questionID, score, domain, facet]
      required: [testSessionId, lang, answers]

    V1SubmitResponse:
      type: object
      properties:
        resultId:
          type: string
          description: Use this as `resultId` when calling `/api/b5plus/v2/reports`.
          pattern: '^[0-9a-fA-F]{24}$'
      required: [resultId]

    V1SubmitPendingResponse:
      type: object
      properties:
        status:
          type: string
          enum: [needs_more, awaiting_finalize]
        remaining:
          type: integer
          minimum: 0
      required: [status, remaining]

    V1SubmitMoreRequiredError:
      type: object
      properties:
        error:
          type: string
          enum: ['more answers required']
        remaining:
          type: integer
          minimum: 0
      required: [error, remaining]

    V1DemographicsRequest:
      type: object
      properties:
        testSessionId:
          type: string
          pattern: '^[0-9a-fA-F]{24}$'
        gender:
          type: string
          enum: [M, F]
        birthYear:
          type: integer
          minimum: 1900
        countryCode:
          type: string
          pattern: '^[A-Z]{2}$'
      required: [testSessionId, gender, birthYear, countryCode]

    V1DemographicsResponse:
      type: object
      properties:
        ok: { type: boolean }
        meta:
          type: object
          additionalProperties: true
      required: [ok, meta]

    V2Error:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        code:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true
      required: [ok, code]

    V2StatusResponse:
      type: object
      properties:
        ok: { type: boolean }
        status: { type: string, enum: [healthy] }
        db: { type: string, enum: [connected] }
        timestamp: { type: string, format: date-time }
      required: [ok, status, db, timestamp]

    V2CreditsResponse:
      type: object
      properties:
        ok: { type: boolean }
        credits_remaining: { type: integer, minimum: 0 }
        total_granted: { type: integer, minimum: 0 }
        total_used: { type: integer, minimum: 0 }
      required: [ok, credits_remaining, total_granted, total_used]

    CreditsSummary:
      type: object
      properties:
        totalGranted: { type: integer, minimum: 0 }
        totalUsed: { type: integer, minimum: 0 }
        remaining: { type: integer, minimum: 0 }
      required: [totalGranted, totalUsed, remaining]

    UsageRef:
      type: object
      properties:
        computationId: { type: string }
        reportId: { type: string }
        entitlementId: { type: string }
        externalRequestId: { type: string }
      additionalProperties: false

    UsageEntry:
      type: object
      properties:
        _id: { type: string }
        resellerId: { type: string }
        kind: { type: string, enum: [credit, debit] }
        amount: { type: number }
        reason: { type: string, enum: [manual_adjustment, test_completed, report_unlocked] }
        note: { type: string }
        ref:
          $ref: '#/components/schemas/UsageRef'
        idempotencyKey: { type: string }
        createdAt: { type: string, format: date-time }
      required: [_id, resellerId, kind, amount, reason, createdAt]

    V2UsageResponse:
      type: object
      properties:
        ok: { type: boolean }
        window:
          type: object
          properties:
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
          required: [from, to]
        credits_summary:
          $ref: '#/components/schemas/CreditsSummary'
        usage:
          type: array
          items:
            $ref: '#/components/schemas/UsageEntry'
      required: [ok, window, credits_summary, usage]

    CatalogMetric:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        summary: { type: string }
        bundles:
          type: array
          items: { type: string }
        preview: { type: string }
      required: [id, title, summary, bundles, preview]

    CatalogBundle:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        metric_ids:
          type: array
          items: { type: string }
      required: [id, name, description, metric_ids]

    ReportStatus:
      type: string
      enum: [requested, processing, complete]

    ReportPayload:
      type: object
      properties:
        report_id: { type: string }
        status: { $ref: '#/components/schemas/ReportStatus' }
        locale: { type: string }
        customer_ref: { type: string }
        requested_with:
          type: string
          enum: [answers, resultId, ext_result_id]
        result_id: { type: string }
        ext_result_id: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        bundles:
          type: array
          items: { type: string }
      required: [report_id, status, requested_with, created_at, updated_at, bundles]

    V2CreateReportRequest:
      type: object
      properties:
        resultId:
          type: string
          description: The `resultId` returned by `/api/v1/tests/submit`.
        ext_result_id:
          type: string
          description: Alternate ext result ID source (optional).
        locale:
          type: string
          enum: [en, ar, es]
          default: en
        customer_ref:
          type: string
          description: Your customer identifier (optional; enables customer grants).
        bundles:
          type: array
          items: { type: string }
          description: Bundle IDs to associate with the report (defaults to `elite`).
        answers:
          type: array
          items: { type: number }
          description: Optional raw numeric answers (advanced/internal; most partners use `resultId`).
      anyOf:
        - required: [resultId]
        - required: [ext_result_id]

    V2CreateReportResponse:
      type: object
      properties:
        ok: { type: boolean }
        report_id: { type: string }
        status: { $ref: '#/components/schemas/ReportStatus' }
        reused: { type: boolean }
        bundles:
          type: array
          items: { type: string }
        report:
          $ref: '#/components/schemas/ReportPayload'
      required: [ok, report_id, status, reused, bundles, report]

    MetricProfileType:
      type: string
      enum: [numeric_index, categorical_style, composite, flag]

    NumericMetricValue:
      type: object
      properties:
        score: { type: number }
        percentile: { type: integer, minimum: 0, maximum: 100 }
        z_score: { type: number }
        confidence: { type: number }
      required: [score, percentile]

    CategoricalMetricValue:
      type: object
      properties:
        label: { type: string }
        percentile: { type: integer, minimum: 0, maximum: 100 }
        distribution:
          type: object
          additionalProperties: { type: number }
      required: [label]

    CompositeFacetValue:
      type: object
      properties:
        facet_key: { type: string }
        score: { type: number }
        percentile: { type: integer, minimum: 0, maximum: 100 }
      required: [facet_key, score]

    CompositeAggregateValue:
      type: object
      properties:
        score: { type: number }
        percentile: { type: integer, minimum: 0, maximum: 100 }
        trend: { type: string, enum: [rising, steady, cooling] }
      required: [score]

    CompositeMetricValue:
      type: object
      properties:
        facets:
          type: array
          items:
            $ref: '#/components/schemas/CompositeFacetValue'
        aggregate:
          $ref: '#/components/schemas/CompositeAggregateValue'
      required: [facets]

    FlagMetricValue:
      type: object
      properties:
        value: { type: boolean }
        reason_codes:
          type: array
          items: { type: string }
      required: [value]

    ApiMetric:
      type: object
      properties:
        key: { type: string }
        profile: { $ref: '#/components/schemas/MetricProfileType' }
        value:
          oneOf:
            - $ref: '#/components/schemas/NumericMetricValue'
            - $ref: '#/components/schemas/CategoricalMetricValue'
            - $ref: '#/components/schemas/CompositeMetricValue'
            - $ref: '#/components/schemas/FlagMetricValue'
        computed_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        source: { type: string, enum: [engine] }
        model_version: { type: string }
      required: [key, profile, value, computed_at, updated_at, source]

    MetricEntitlement:
      type: object
      properties:
        metricId: { type: string }
        title: { type: string }
        summary: { type: string }
        bundles:
          type: array
          items: { type: string }
        unlocked: { type: boolean }
        preview: { type: string }
      required: [metricId, title, summary, bundles, unlocked, preview]

    ReportEntitlements:
      type: object
      properties:
        report:
          type: array
          items: { type: string }
        customer:
          type: array
          items: { type: string }
        effective:
          type: array
          items: { type: string }
      required: [report, customer, effective]

    ReportEntitlementItems:
      type: object
      properties:
        unlocked:
          type: array
          items: { $ref: '#/components/schemas/MetricEntitlement' }
        locked:
          type: array
          items: { $ref: '#/components/schemas/MetricEntitlement' }
      required: [unlocked, locked]

    EngineMetric:
      type: object
      properties:
        id:
          type: string
          description: Engine metric ID (e.g. `bounceBack`, `commStyle`, `pillars`, `roleFit`).
        value:
          description: Raw engine metric payload (shape varies by metric).
          nullable: true
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items: {}
            - type: string
            - type: number
            - type: boolean
      required: [id, value]

    OrganizedMetric:
      allOf:
        - $ref: '#/components/schemas/EngineMetric'
        - type: object
          properties:
            text:
              description: Localized metric text for this metric (title, labels, introShort).
              type: object
              nullable: true
              additionalProperties: true
            info_cards:
              description: Localized info copy grouped for this metric (`main`, and optional `related`).
              type: object
              nullable: true
              additionalProperties: true
          required: [text, info_cards]

    V2ReportContent:
      type: object
      description: Localized UI text helpers for partners (titles, labels, introShort, and info cards).
      properties:
        metric_text:
          type: object
          description: Same shape as `GET /api/b5plus/v2/metric-text?locale=...`.
          additionalProperties: true
        info_cards:
          type: object
          description: Localized info popover copy (the `infoCards` namespace).
          additionalProperties: true
      required: [metric_text, info_cards]

    V2ReportPayloadFull:
      allOf:
        - $ref: '#/components/schemas/ReportPayload'
        - type: object
          properties:
            computed_at:
              type: string
              format: date-time
              description: Timestamp when engine metrics were computed for this response.
            algorithm_version:
              type: string
              description: Paid engine algorithm version stamp.
            norm_version:
              type: string
              description: Norm version stamp (when available).
            display_report:
              type: object
              description: Full paid engine `displayReport` JSON (includes roleFit, vpi, commStyle, strengths, etc.). Shape may evolve.
              additionalProperties: true
            extras:
              type: object
              description: Paid engine `extras` JSON (UI helper values). Shape may evolve.
              additionalProperties: true
            content:
              $ref: '#/components/schemas/V2ReportContent'
          required: [computed_at, algorithm_version, display_report, extras, content]

    V2ReportPayloadMetrics:
      allOf:
        - $ref: '#/components/schemas/ReportPayload'
        - type: object
          properties:
            computed_at:
              type: string
              format: date-time
              description: Timestamp when engine metrics were computed for this response.
            algorithm_version:
              type: string
              description: Paid engine algorithm version stamp.
            norm_version:
              type: string
              description: Norm version stamp (when available).
            metrics:
              type: array
              items: { $ref: '#/components/schemas/EngineMetric' }
            content:
              $ref: '#/components/schemas/V2ReportContent'
          required: [computed_at, algorithm_version, metrics, content]

    V2ReportPayloadOrganized:
      allOf:
        - $ref: '#/components/schemas/ReportPayload'
        - type: object
          properties:
            computed_at:
              type: string
              format: date-time
              description: Timestamp when engine metrics were computed for this response.
            algorithm_version:
              type: string
              description: Paid engine algorithm version stamp.
            norm_version:
              type: string
              description: Norm version stamp (when available).
            metrics:
              type: array
              items: { $ref: '#/components/schemas/OrganizedMetric' }
            sections:
              type: object
              description: Narrative sections from the full report (intro, strengths, weaknesses, advice, conclusion).
              additionalProperties: true
            pct_map:
              type: object
              description: Big Five facet percentile map (facet -> percentile).
              nullable: true
              additionalProperties: true
            norm_sample_size:
              type: integer
              description: Size of the norm sample used for percentiles.
              nullable: true
          required: [computed_at, algorithm_version, metrics, sections]
    V2ReportPayloadBoth:
      allOf:
        - $ref: '#/components/schemas/V2ReportPayloadFull'
        - type: object
          properties:
            metrics:
              type: array
              items: { $ref: '#/components/schemas/EngineMetric' }
          required: [metrics]

    V2ReportGetResponse:
      type: object
      properties:
        ok: { type: boolean }
        report:
          oneOf:
            - $ref: '#/components/schemas/V2ReportPayloadFull'
            - $ref: '#/components/schemas/V2ReportPayloadMetrics'
            - $ref: '#/components/schemas/V2ReportPayloadOrganized'
            - $ref: '#/components/schemas/V2ReportPayloadBoth'
      required: [ok, report]

  /api/b5plus/v2/status:
    get:
      tags: [Partner (v2)]
      summary: Service status
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
      responses:
        '200':
          description: Service is healthy.
          headers:
            X-Request-Id:
              description: Correlates this response with internal logs.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2StatusResponse'
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '5XX':
          description: Unexpected error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/credits:
    get:
      tags: [Partner (v2)]
      summary: Get current credit balance
      description: Returns current credits remaining and totals from the usage ledger.
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
      responses:
        '200':
          description: Credit balance.
          headers:
            X-Credits-Remaining:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2CreditsResponse'
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '5XX':
          description: Unexpected error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/usage:
    get:
      tags: [Partner (v2)]
      summary: List usage ledger entries in a time window
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Window start (ISO date/time).
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Window end (ISO date/time).
      responses:
        '200':
          description: Usage ledger window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2UsageResponse'
        '400':
          description: Invalid window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/catalog/metrics:
    get:
      tags: [Catalog (v2)]
      summary: List available metrics
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
      responses:
        '200':
          description: Metric catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  metrics:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogMetric'
                required: [ok, metrics]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/catalog/bundles:
    get:
      tags: [Catalog (v2)]
      summary: List available bundles
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
      responses:
        '200':
          description: Bundle catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  bundles:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogBundle'
                required: [ok, bundles]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports:
    post:
      tags: [Reports (v2)]
      summary: Create a paid report (costs 1 credit)
      description: |
        Creates a report computation and debits 1 credit (if a new report is created).

        This endpoint is idempotent: provide `Idempotency-Key` to safely retry.
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/V2CreateReportRequest'
      responses:
        '200':
          description: Report created (or reused).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2CreateReportResponse'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '402':
          description: Insufficient credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '409':
          description: Idempotency conflict.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports/{id}:
    get:
      tags: [Reports (v2)]
      summary: Get report (engine metrics)
      description: |
        Returns the report payload and engine-computed JSON.

        - `format=full` (default): returns the full paid engine report (`display_report` + `extras` + `content`).
        - `format=metrics`: returns the 33-metric list (`metrics: [{id,value}]`) plus `content`.
        - `format=organized`: returns partner-friendly per-metric objects (`metrics: [{id,value,text,info_cards}]`) plus `sections`, `pct_map`, and `norm_sample_size`.
        - `format=both`: returns both (redundant; not recommended).
        - `metric_keys` (only for `format=metrics|organized|both`) filters the returned metric list (comma-separated IDs).
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [full, metrics, organized, both]
            default: full
          description: Select response shape.
        - name: metric_keys
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated metric IDs (only for `format=metrics|organized|both`, e.g. `bounceBack,commStyle`).
      responses:
        '200':
          description: Report returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2ReportGetResponse'
        '400':
          description: Invalid request (including unknown metric keys).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '404':
          description: Report not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports/{id}/status:
    get:
      tags: [Reports (v2)]
      summary: Get report status
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Status returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  report_id: { type: string }
                  status: { $ref: '#/components/schemas/ReportStatus' }
                  report: { $ref: '#/components/schemas/ReportPayload' }
                required: [ok, report_id, status, report]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '404':
          description: Report not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports/{id}/unlock:
    post:
      tags: [Reports (v2)]
      summary: Add bundles to a report entitlement
      description: Adds bundle IDs to the report's entitlement bundle list.
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                bundle_ids:
                  type: array
                  items:
                    type: string
              required: []
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  report_id: { type: string }
                  bundles:
                    type: array
                    items: { type: string }
                required: [ok, report_id, bundles]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '404':
          description: Report not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/customers/{ref}/grants:
    post:
      tags: [Customers (v2)]
      summary: Update customer bundle grants
      description: |
        Adds/removes bundles for a customer reference. Effective report entitlements are:
        `effective = report.bundles ∪ customer.bundles`.
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: ref
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                add:
                  type: array
                  items: { type: string }
                remove:
                  type: array
                  items: { type: string }
      responses:
        '200':
          description: Updated grants.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  customer_ref: { type: string }
                  bundles:
                    type: array
                    items: { type: string }
                required: [ok, customer_ref, bundles]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/customers/{ref}/reports:
    get:
      tags: [Customers (v2)]
      summary: List reports for a customer ref
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: ref
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Reports list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  customer_ref: { type: string }
                  reports:
                    type: array
                    items: { $ref: '#/components/schemas/ReportPayload' }
                required: [ok, customer_ref, reports]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports/{id}/view-links:
    post:
      tags: [View Links (v2)]
      summary: Create a signed JSON view link (optional)
      description: Returns a signed link that can be used to fetch the report JSON without partner auth.
      parameters:
        - $ref: '#/components/parameters/XProviderHeader'
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ttl_sec:
                  type: integer
                  minimum: 1
                  description: Time-to-live in seconds (default 3600).
      responses:
        '200':
          description: Signed view link created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  report: { $ref: '#/components/schemas/ReportPayload' }
                  link: { type: string }
                  expires_at: { type: string, format: date-time }
                required: [ok, report, link, expires_at]
        '401':
          description: Auth failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '404':
          description: Report not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/b5plus/v2/reports/view:
    get:
      tags: [View Links (v2)]
      summary: Fetch report JSON via signed link (optional)
      description: Public endpoint (no partner auth). Requires signed query params from `/view-links`.
      security: []
      parameters:
        - name: rid
          in: query
          required: true
          schema: { type: string }
        - name: exp
          in: query
          required: true
          schema: { type: integer }
          description: Unix epoch seconds.
        - name: sig
          in: query
          required: true
          schema: { type: string }
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [full, metrics, organized, both]
            default: full
          description: Select response shape.
        - name: metric_keys
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated metric IDs (only for `format=metrics|organized|both`).
      responses:
        '200':
          description: Report returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2ReportGetResponse'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '401':
          description: Invalid/expired signature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'
        '404':
          description: Report not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V2Error'

  /api/v1/tests/items:
    get:
      tags: [Assessment (v1)]
      summary: Fetch assessment questions (batched)
      description: |
        Returns a batch of canonical Big Five question items (with translations).

        Call this repeatedly until you receive all items.
      parameters:
        - name: testSessionId
          in: query
          required: true
          description: The assessment session ID returned by `/api/v1/tests/start`.
          schema:
            type: string
            pattern: '^[0-9a-fA-F]{24}$'
        - name: lang
          in: query
          required: true
          schema:
            type: string
            enum: [en, ar, es]
        - name: batch
          in: query
          required: false
          description: Number of questions to fetch in one call (max 20).
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
      responses:
        '200':
          description: Items returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1ItemsResponse'
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '401':
          description: Missing/invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '409':
          description: Session already submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'

  /api/v1/tests/submit:
    post:
      tags: [Assessment (v1)]
      summary: Submit answers (and optionally finalize)
      description: |
        Submits a list of answers for the session.

        - If not all answers are submitted, the API returns `202` with `status=needs_more`.
        - If all answers are submitted but `finalize=false`, the API returns `202` with `status=awaiting_finalize`.
        - If all answers are submitted and `finalize=true`, the API returns `200` with `resultId`.

        The returned `resultId` is used to create a paid report in v2.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/V1SubmitRequest'
      responses:
        '200':
          description: Finalized; result created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1SubmitResponse'
        '202':
          description: Accepted; more steps required before finalizing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1SubmitPendingResponse'
        '400':
          description: Bad request (including finalizing too early).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/V1Error'
                  - $ref: '#/components/schemas/V1SubmitMoreRequiredError'
        '401':
          description: Missing/invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '409':
          description: Session already submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'

  /api/v1/tests/demographics:
    post:
      tags: [Assessment (v1)]
      summary: Save demographics (optional)
      description: Optional helper endpoint for storing demographics associated with a session/result.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/V1DemographicsRequest'
      responses:
        '200':
          description: Demographics saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1DemographicsResponse'
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '401':
          description: Missing/invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
        '429':
          description: Rate limited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/V1Error'
