openapi: 3.0.3
info:
  title: QuickContact API
  version: v1-draft
  description: |
    Conservative draft OpenAPI document generated from current repo truth.
    This file is intended to help integrators explore the surface area that is already real.
    It is not yet the sole canonical contract source, and it should not be read as a finalized statement
    of pricing, quota semantics, billing interpretation, or every live operational nuance.
servers:
  - url: https://your-quickcontact-host
paths:
  /health:
    get:
      summary: Health check
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiHealthResponse'
  /v1/mode-recommendation:
    get:
      summary: Get recommended capture mode for a country
      parameters:
        - in: query
          name: country
          schema: { type: string }
        - in: query
          name: userCountry
          schema: { type: string }
      responses:
        '200':
          description: Mode recommendation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModeRecommendationResponseDto'
  /v1/resolve:
    post:
      summary: Resolve a phone number using page context
      parameters:
        - in: header
          name: X-SaveCall-Api-Key
          schema: { type: string }
          required: false
          description: Required when clientRef is supplied and enforcement applies.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/ResolveRequestDto'
                - type: object
                  properties:
                    clientRef:
                      type: string
                      description: Current server-supported tenant identifier used for auth, billing, and quota enforcement.
      responses:
        '200':
          description: Resolve response
          headers:
            X-SaveCall-Warning:
              schema: { type: string }
            X-SaveCall-Plan:
              schema: { type: string }
              description: Runtime plan/quota signal. Interpret cautiously while plan mapping remains operationally evolving.
            X-SaveCall-Quota-Limit:
              schema: { type: integer }
            X-SaveCall-Quota-Used:
              schema: { type: integer }
            X-SaveCall-Quota-Remaining:
              schema: { type: integer }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolveResponseDto'
        '400':
          description: Bad request or strict-mode clientRef failure
        '401':
          description: Missing or invalid API key
        '402':
          description: Inactive subscription
        '403':
          description: clientRef mismatch
        '429':
          description: Plan quota exceeded
  /v1/feedback/resolution:
    post:
      summary: Submit resolution feedback
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResolutionFeedbackRequestDto'
      responses:
        '202':
          description: Feedback accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiAckResponse'
  /billing/checkout:
    post:
      summary: Create Stripe Checkout Session
      description: Accepts current billing identifiers used by the server. These identifiers are not documented here as final public pricing policy.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan:
                  type: string
                  enum: [DEVELOPER, TEAM, GROWTH]
                clientRef:
                  type: string
      responses:
        '200':
          description: Checkout session created
  /billing/portal:
    post:
      summary: Create Stripe Customer Portal Session
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                clientRef:
                  type: string
                customerId:
                  type: string
      responses:
        '200':
          description: Portal session created
  /billing/state:
    get:
      summary: Read effective billing state for a clientRef
      description: Reads locally persisted webhook-derived billing state. isActive is the most reliable public interpretation; plan and price fields may be null or provisional.
      parameters:
        - in: query
          name: clientRef
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Billing state
  /stripe/webhook:
    post:
      summary: Internal Stripe webhook endpoint
      description: Not a general client-facing endpoint.
      responses:
        '200':
          description: Event received
components:
  schemas:
    ApiHealthResponse:
      type: object
      properties:
        status: { type: string }
        service: { type: string }
        version: { type: string }
      required: [status, service, version]
    ApiOriginContext:
      type: object
      properties:
        url: { type: string, nullable: true }
        title: { type: string, nullable: true }
        host: { type: string, nullable: true }
        canonicalUrl: { type: string, nullable: true }
        nearHeading: { type: string, nullable: true }
        selectedText: { type: string, nullable: true }
        pageExcerpt: { type: string, nullable: true }
        anchorRowText: { type: string, nullable: true }
        anchorPrecedingLabel: { type: string, nullable: true }
        anchorContainerText: { type: string, nullable: true }
        anchorInlineText: { type: string, nullable: true }
    ApiResolvePreferences:
      type: object
      properties:
        confirmBeforeSaving: { type: boolean }
        allowNumberOnly: { type: boolean }
        preferEntityNameForTelLink: { type: boolean }
    ResolveRequestDto:
      type: object
      required: [rawNumber, userCountry, searchCountry, searchLanguage, captureMode, sourceType]
      properties:
        rawNumber: { type: string }
        userCountry: { type: string }
        searchCountry: { type: string }
        searchLanguage: { type: string }
        captureMode:
          type: string
          enum: [EXTENSION, CHROME]
        sourceType:
          type: string
          enum: [TEL_LINK, SELECTION, CONTEXT_MENU, PAGE_DETECTION, MANUAL, UNKNOWN]
        browser: { type: string, nullable: true }
        origin:
          $ref: '#/components/schemas/ApiOriginContext'
        preferences:
          $ref: '#/components/schemas/ApiResolvePreferences'
    ApiConfidence:
      type: object
      properties:
        score: { type: number, format: float }
        level:
          type: string
          enum: [HIGH, MEDIUM, LOW]
        label: { type: string }
    ApiResolvedMatch:
      type: object
      properties:
        id: { type: string }
        displayName: { type: string }
        phoneE164: { type: string }
        sourceUrl: { type: string, nullable: true }
        canonicalSourceUrl: { type: string, nullable: true }
        sourceLabel: { type: string, nullable: true }
        entityType: { type: string, nullable: true }
        reasonSummary: { type: string, nullable: true }
    ApiAlternativeMatch:
      type: object
      properties:
        id: { type: string }
        displayName: { type: string }
        phoneE164: { type: string }
        sourceLabel: { type: string, nullable: true }
        sourceUrl: { type: string, nullable: true }
        confidenceScore: { type: number, format: float }
        reasonShort: { type: string, nullable: true }
    ResolveResponseDto:
      type: object
      properties:
        resolutionId: { type: string }
        status:
          type: string
          enum: [RESOLVED, NUMBER_ONLY, AMBIGUOUS, NOT_FOUND, ERROR]
        recommendedAction:
          type: string
          enum: [SAVE_CONTACT, SAVE_NUMBER_ONLY, REVIEW_ALTERNATIVES, RETRY, NONE]
        userMessage: { type: string, nullable: true }
        allowNumberOnly: { type: boolean }
        normalizedNumber: { type: string, nullable: true }
        confidence:
          $ref: '#/components/schemas/ApiConfidence'
        bestMatch:
          $ref: '#/components/schemas/ApiResolvedMatch'
        alternatives:
          type: array
          items:
            $ref: '#/components/schemas/ApiAlternativeMatch'
        contextProvenance: { type: string, nullable: true }
        debugTraceId: { type: string, nullable: true }
    ApiModeBenchmark:
      type: object
      properties:
        mode:
          type: string
          enum: [EXTENSION, CHROME]
        successRate: { type: number, format: float }
        sampleSize: { type: integer }
        updatedAtUtc: { type: string }
        freshness:
          type: string
          enum: [FRESH, AGING, STALE]
    ModeRecommendationResponseDto:
      type: object
      properties:
        countryCode: { type: string }
        recommendedMode:
          type: string
          enum: [EXTENSION, CHROME]
        reason: { type: string }
        benchmarks:
          type: array
          items:
            $ref: '#/components/schemas/ApiModeBenchmark'
        defaultSearchCountry: { type: string }
        defaultSearchLanguage: { type: string }
        sourceLabel: { type: string }
        fallbackUsed: { type: boolean }
    ResolutionFeedbackRequestDto:
      type: object
      required: [resolutionId, action]
      properties:
        resolutionId: { type: string }
        action:
          type: string
          enum: [ACCEPTED_BEST_MATCH, ACCEPTED_ALTERNATIVE, SAVED_NUMBER_ONLY, REJECTED, EDITED_NAME_BEFORE_SAVE, ABANDONED]
        savedAsName: { type: string, nullable: true }
        selectedAlternativeIndex: { type: integer, nullable: true }
        editedName: { type: boolean }
    ApiAckResponse:
      type: object
      properties:
        accepted: { type: boolean }
        message: { type: string }
      required: [accepted, message]
