openapi: 3.0.3
info:
  title: travel-mcp (HTTP API)
  version: 0.1.0
  description: >
    HTTP endpoints exposed by the travel-mcp service (health, status, dashboard control-plane, Stripe webhooks).

    Note: the MCP endpoint (`/mcp`) is included as an opaque protocol surface (MCP Streamable HTTP), not a traditional REST API.
servers:
  - url: /
tags:
  - name: meta
  - name: status
  - name: mcp
  - name: oauth
  - name: dashboard
  - name: stripe
  - name: feedback
components:
  securitySchemes:
    adminBearer:
      type: http
      scheme: bearer
      bearerFormat: opaque
    dashboardBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    Health:
      type: object
      additionalProperties: false
      properties:
        status:
          type: string
          example: ok
      required: [status]
    OAuthProtectedResourceMetadata:
      type: object
      additionalProperties: true
      properties:
        resource:
          type: string
          format: uri
        authorization_servers:
          type: array
          items:
            type: string
            format: uri
        bearer_methods_supported:
          type: array
          items:
            type: string
        scopes_supported:
          type: array
          items:
            type: string
        resource_documentation:
          type: string
          format: uri
      required: [resource, authorization_servers, bearer_methods_supported]
    DashboardError:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: false
        error:
          type: string
      required: [ok, error]
    Whoami:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        owner_id:
          type: string
        org_id:
          type: string
          nullable: true
        user_id:
          type: string
          nullable: true
      required: [ok, owner_id]
    DashboardMetricsResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        summary:
          type: object
          additionalProperties: false
          properties:
            total_requests:
              type: integer
              format: int64
            avg_latency_ms:
              type: integer
              format: int64
            success_rate_pct:
              type: number
            metrics_window_days:
              type: integer
          required: [total_requests, avg_latency_ms, success_rate_pct, metrics_window_days]
        trends:
          type: object
          additionalProperties: false
          properties:
            total_requests_pct:
              type: number
              nullable: true
            avg_latency_pct:
              type: number
              nullable: true
            success_rate_pct:
              type: number
              nullable: true
        traffic:
          type: object
          additionalProperties: false
          properties:
            window_days:
              type: integer
            points:
              type: array
              items:
                type: object
                additionalProperties: false
                properties:
                  date:
                    type: string
                    example: "2026-02-18"
                  label:
                    type: string
                    example: "Feb 18"
                  requests:
                    type: integer
                    format: int64
                required: [date, label, requests]
          required: [window_days, points]
      required: [ok, summary, trends, traffic]
    UnkeyKey:
      type: object
      description: >
        Unkey key metadata as returned by the Unkey admin API. Shape may evolve; treat as best-effort.
      additionalProperties: true
    DashboardAPIKeysListResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        cursor:
          type: string
          nullable: true
        keys:
          type: array
          items:
            $ref: "#/components/schemas/UnkeyKey"
      required: [ok, keys]
    DashboardAPIKeyCreateRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        prefix:
          type: string
        byte_length:
          type: integer
          description: "Default 32; allowed range 16..64."
      required: []
    DashboardAPIKeyCreateResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        key_id:
          type: string
        api_key:
          type: string
          description: "Plaintext key returned once."
        owner:
          type: object
          additionalProperties: false
          properties:
            owner_id:
              type: string
            org_id:
              type: string
              nullable: true
            user_id:
              type: string
              nullable: true
          required: [owner_id]
        note:
          type: string
      required: [ok, key_id, api_key]
    DashboardKeyIDRequest:
      type: object
      additionalProperties: false
      properties:
        key_id:
          type: string
      required: [key_id]
    DashboardAPIKeyRotateRequest:
      type: object
      additionalProperties: false
      properties:
        key_id:
          type: string
        name:
          type: string
        prefix:
          type: string
      required: [key_id]
    DashboardAPIKeyRotateResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        key_id:
          type: string
        api_key:
          type: string
        note:
          type: string
      required: [ok, key_id, api_key]
    BillingCheckoutRequest:
      type: object
      additionalProperties: false
      properties:
        price_id:
          type: string
        ui_mode:
          type: string
          description: "hosted (default) or embedded"
          enum: [hosted, embedded]
      required: []
    BillingCheckoutResponseHosted:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        ui_mode:
          type: string
          enum: [hosted]
        session_id:
          type: string
        url:
          type: string
      required: [ok, ui_mode, session_id, url]
    BillingCheckoutResponseEmbedded:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        ui_mode:
          type: string
          enum: [embedded]
        session_id:
          type: string
        client_secret:
          type: string
      required: [ok, ui_mode, session_id, client_secret]
    BillingPortalResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        url:
          type: string
      required: [ok, url]
    StripeWebhookResponse:
      type: object
      additionalProperties: false
      properties:
        ok:
          type: boolean
          example: true
        duplicate:
          type: boolean
        ignored:
          type: boolean
      required: [ok]
    StripeIdempotencyServiceRequest:
      type: object
      additionalProperties: false
      properties:
        op:
          type: string
          enum: [try_mark, commit, unmark]
        event_id:
          type: string
        ttl_sec:
          type: integer
          format: int64
          description: "Required for try_mark and commit."
      required: [op, event_id]
    StripeIdempotencyTryMarkResponse:
      type: object
      additionalProperties: false
      properties:
        is_new:
          type: boolean
      required: [is_new]
    LiveStatus:
      type: object
      description: "Public status snapshot; shape may evolve."
      additionalProperties: true
paths:
  /:
    get:
      tags: [meta]
      summary: Landing page
      responses:
        "200":
          description: HTML landing page
          content:
            text/html:
              schema:
                type: string
  /openapi.yaml:
    get:
      tags: [meta]
      summary: OpenAPI spec (YAML)
      responses:
        "200":
          description: OpenAPI document
          content:
            application/yaml:
              schema:
                type: string
  /.well-known/agent-card.json:
    get:
      tags: [meta]
      summary: A2A Agent Card (discovery)
      description: >
        Returns a Google A2A agent card describing this service's capabilities,
        skills, supported interfaces (MCP, OpenAPI), and accepted authentication
        methods (API key, x402 USDC payment). The card includes transaction rails
        for random agents: canonical discovery URLs, rotate-vs-refresh provisioning
        behavior, non-billable MCP calls, billable MCP tools, and renewal endpoints.
      responses:
        "200":
          description: Agent card JSON
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  name:
                    type: string
                  description:
                    type: string
                  url:
                    type: string
                  version:
                    type: string
                  skills:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  supportedInterfaces:
                    type: object
                    additionalProperties: true
                  payment:
                    type: object
                    additionalProperties: true
        "304":
          description: Not Modified (ETag match)
  /x402/api-keys:
    post:
      tags: [meta]
      summary: Provision an API key via x402 payment (paid)
      description: >
        Pay via x402 once, then receive or refresh a paid Unkey API key.
        Settlement is attempted only after the API key has been successfully created.
        Subsequent MCP requests are metered through that API key's normal quota/rate-limit
        policy rather than charged again on-chain.
        The plaintext key is only returned when a new key is minted or when the same
        payment is replayed within the replay window. Repurchases may refresh quota on
        an existing key and return only the `key_id`, requiring the caller to reuse the
        key they already stored. Agents that require fresh plaintext should set
        `rotate_existing=true` so the service rotates the payer's current paid key
        and returns a new `api_key` in the success response.
      x-agent-transaction-rails:
        canonical_key_header: x-api-key
        payment_required_header: PAYMENT-REQUIRED
        payment_signature_header: PAYMENT-SIGNATURE
        default_mode:
          request_body: {}
          behavior: refreshes quota on the payer's existing paid key when possible
          api_key_returned: only on created or replayed responses
        rotate_existing_mode:
          request_body:
            rotate_existing: true
          behavior: rotates the payer's existing paid key and returns fresh plaintext
          api_key_returned: expected on success
        response_statuses:
          created: new paid key created and plaintext returned
          replayed: previous successful payment replayed and plaintext returned again inside the replay window
          quota_refreshed: existing paid key quota refreshed; reuse stored key plaintext
          rotated: existing paid key rotated; use newly returned plaintext
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                prefix:
                  type: string
                byte_length:
                  type: integer
                  description: "Default 32; allowed range 16..64."
                rotate_existing:
                  type: boolean
                  description: "When true, repurchase rotates the payer's existing paid key and returns fresh plaintext. When omitted or false, repurchase may refresh quota on the existing key without returning plaintext."
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                  status:
                    type: string
                    description: "created, replayed, quota_refreshed, or rotated"
                  key_id:
                    type: string
                  api_key:
                    type: string
                    description: "Plaintext key, returned only when available."
                  api_key_returned:
                    type: boolean
                    description: "False when quota was refreshed on an existing key and plaintext is not returned."
                  payer:
                    type: string
                    description: "Best-effort payer wallet address (facilitator-dependent)."
                  note:
                    type: string
                required: [ok, key_id]
        "402":
          description: Payment required. Read the `PAYMENT-REQUIRED` response header and pay exactly that advertised offer.
        "501":
          description: Unkey not configured or x402 disabled
    options:
      tags: [meta]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /x402/api-keys/trial:
    post:
      tags: [meta]
      summary: Provision a trial API key via x402 verification (no settlement)
      description: >
        Verify a wallet via x402 (no on-chain settlement), then mint a limited Unkey API key.
        Trial keys are one-per-wallet and expire after a short duration. Subsequent MCP
        requests are tracked against the issued key's quota, not re-verified per request.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                  key_id:
                    type: string
                  api_key:
                    type: string
                  wallet:
                    type: string
                  expires_at:
                    type: string
                  limit:
                    type: integer
                  duration_s:
                    type: integer
                  note:
                    type: string
                required: [ok, key_id, api_key, wallet, expires_at]
        "402":
          description: Verification required (x402 signature missing/invalid)
        "409":
          description: Trial already issued for wallet
        "501":
          description: Unkey not configured, database missing, or facilitator cannot provide payer address
    options:
      tags: [meta]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /x402/automation-sessions:
    post:
      tags: [meta]
      summary: Create a bounded x402 automation session
      description: >
        Stores a batch of pre-signed x402 payment vouchers so an agent can renew or rotate
        its paid API key later without handing the service an unrestricted wallet. The
        current API key is validated without consuming request quota and is encrypted at rest
        together with the submitted vouchers. The response returns a one-time recovery token
        that can recover the latest active API key for this automation session.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                payment_signatures:
                  type: array
                  minItems: 1
                  items:
                    type: string
                  description: Pre-signed x402 vouchers compatible with the current paid offer.
                auto_rotate:
                  type: boolean
                  description: When true, automated renewals rotate the current paid key.
                max_amount_atomic:
                  type: string
                  description: Optional per-voucher ceiling in atomic USDC units.
              required: [payment_signatures]
      responses:
        "201":
          description: Automation session created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                  session_id:
                    type: string
                  recovery_token:
                    type: string
                    description: Returned once; store securely.
                  auto_rotate:
                    type: boolean
                  max_amount_atomic:
                    type: string
                  total_budget_atomic:
                    type: string
                  vouchers_total:
                    type: integer
                  vouchers_remaining:
                    type: integer
                  key_id:
                    type: string
                  renew_endpoint:
                    type: string
                  recover_endpoint:
                    type: string
                  note:
                    type: string
                required:
                  - ok
                  - session_id
                  - recovery_token
                  - vouchers_total
                  - vouchers_remaining
                  - renew_endpoint
                  - recover_endpoint
        "400":
          description: Invalid or incompatible voucher payload
        "401":
          description: Missing or invalid API key
        "501":
          description: Database, encryption key, or x402 automation support is unavailable
    options:
      tags: [meta]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /x402/automation-sessions/renew:
    post:
      tags: [meta]
      summary: Renew a paid x402 key from a bounded automation session
      description: >
        Consumes the next stored pre-signed voucher for the named automation session and
        invokes the normal paid x402 provisioning flow. This endpoint validates the current
        API key without charging additional usage so an exhausted key can still renew if
        budget remains.
      x-agent-transaction-rails:
        current_key_validation: non-billable
        exhaustion_recovery: allows renewal even when the current key has no purchased quota remaining
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                session_id:
                  type: string
              required: [session_id]
      responses:
        "200":
          description: Renewal or rotation completed
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  ok:
                    type: boolean
                  key_id:
                    type: string
                  api_key:
                    type: string
                    description: Present when a fresh plaintext key is returned.
                  api_key_returned:
                    type: boolean
                  automation:
                    type: object
                    additionalProperties: false
                    properties:
                      session_id:
                        type: string
                      vouchers_total:
                        type: integer
                      vouchers_used:
                        type: integer
                      vouchers_remaining:
                        type: integer
                      auto_rotate:
                        type: boolean
                      max_amount_atomic:
                        type: string
                      renewed_via:
                        type: string
                    required:
                      - session_id
                      - vouchers_total
                      - vouchers_used
                      - vouchers_remaining
                      - auto_rotate
                      - max_amount_atomic
                      - renewed_via
        "400":
          description: Invalid request body
        "401":
          description: Missing or invalid API key
        "409":
          description: Automation session not found for this key or no voucher budget remains
        "501":
          description: Database, encryption key, or x402 automation support is unavailable
    options:
      tags: [meta]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /x402/automation-sessions/recover:
    post:
      tags: [meta]
      summary: Recover the current key for an x402 automation session
      description: >
        Returns the current active API key for a bounded automation session when the caller
        presents the one-time recovery token returned at session creation. This endpoint does
        not require the current API key, which allows durable session reuse after local key
        loss or rotation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                session_id:
                  type: string
                recovery_token:
                  type: string
              required: [session_id, recovery_token]
      responses:
        "200":
          description: Current automation session state recovered
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                  session_id:
                    type: string
                  api_key:
                    type: string
                  key_id:
                    type: string
                  auto_rotate:
                    type: boolean
                  max_amount_atomic:
                    type: string
                  total_budget_atomic:
                    type: string
                  vouchers_total:
                    type: integer
                  vouchers_used:
                    type: integer
                  vouchers_remaining:
                    type: integer
                  last_tx_hash:
                    type: string
                  note:
                    type: string
                required:
                  - ok
                  - session_id
                  - api_key
                  - vouchers_total
                  - vouchers_used
                  - vouchers_remaining
        "400":
          description: Invalid request body
        "401":
          description: Missing or invalid recovery token
        "501":
          description: Database, encryption key, or x402 automation support is unavailable
    options:
      tags: [meta]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /feedback:
    post:
      tags: [feedback]
      summary: Submit product feedback without consuming request quota
      description: >
        Submit agent feedback after quota exhaustion or other product friction. This endpoint
        validates the supplied API key without charging additional usage and accepts feedback
        from valid or quota-exhausted keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                message:
                  type: string
                category:
                  type: string
                tool:
                  type: string
                last_error:
                  type: string
              required: [message]
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                  accepted:
                    type: boolean
                required: [ok, accepted]
        "400":
          description: Invalid request body
        "401":
          description: Missing or invalid API key
    options:
      tags: [feedback]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
  /health:
    get:
      tags: [status]
      summary: Health check
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /healthz:
    get:
      tags: [status]
      summary: Health check (alias)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /live:
    get:
      tags: [status]
      summary: Runtime wiring/status (public, no secrets)
      security:
        - adminBearer: []
        - {}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveStatus"
        "401":
          description: Missing/invalid admin bearer token
          content:
            text/plain:
              schema:
                type: string
  /admin/live:
    get:
      tags: [status]
      summary: Runtime wiring/status (alias)
      security:
        - adminBearer: []
        - {}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveStatus"
        "401":
          description: Missing/invalid admin bearer token
          content:
            text/plain:
              schema:
                type: string
  /mcp:
    get:
      tags: [mcp]
      summary: MCP Streamable HTTP endpoint (streaming)
      description: >
        MCP Streamable HTTP (SSE/streaming). Exact protocol is defined by MCP clients/servers.
        Agents should use MCP discovery plus the agent card/OpenAPI rails to distinguish
        non-billable setup and quota inspection from billable search/tool completion.
      responses:
        "200":
          description: Streaming channel
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          description: Unauthorized (missing or invalid credentials)
          headers:
            WWW-Authenticate:
              description: >
                Bearer challenge. When OAuth metadata discovery is enabled, includes `resource_metadata`
                (and optionally `scope`) for MCP client authorization-server discovery.
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
    post:
      tags: [mcp]
      summary: MCP Streamable HTTP endpoint (requests)
      description: >
        MCP Streamable HTTP request channel. Body is MCP protocol JSON.
        Auth/session setup requests such as `initialize`, `notifications/initialized`,
        `tools/list`, `auth_ping`, and `quota_status` do not consume purchased quota.
        Successful billable tool completions such as `search_flights`, `search_hotels`,
        and `get_price_graph` do consume purchased quota. Temporary throttles are
        separate from quota exhaustion; rate-limited tool results include machine-readable
        error fields such as `source`, `scope`, `reason`, and `retry_after_sec`.
      x-agent-transaction-rails:
        non_billable_methods:
          - initialize
          - notifications/initialized
          - tools/list
        non_billable_tools:
          - auth_ping
          - quota_status
        billable_tools_on_success:
          - search_flights
          - search_hotels
          - get_price_graph
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: MCP response
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          description: Unauthorized (missing or invalid credentials)
          headers:
            WWW-Authenticate:
              description: >
                Bearer challenge. When OAuth metadata discovery is enabled, includes `resource_metadata`
                (and optionally `scope`) for MCP client authorization-server discovery.
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
  /.well-known/oauth-protected-resource:
    get:
      tags: [oauth]
      summary: OAuth 2.0 Protected Resource Metadata (root)
      description: >
        RFC9728 metadata document for MCP authorization-server discovery.
      responses:
        "200":
          description: OAuth protected resource metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthProtectedResourceMetadata"
        "404":
          description: OAuth metadata not configured
  /.well-known/oauth-protected-resource/mcp:
    get:
      tags: [oauth]
      summary: OAuth 2.0 Protected Resource Metadata (MCP path-specific)
      description: >
        Path-specific RFC9728 metadata for the MCP endpoint.
      responses:
        "200":
          description: OAuth protected resource metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthProtectedResourceMetadata"
        "404":
          description: OAuth metadata not configured
  /dashboard/whoami:
    get:
      tags: [dashboard]
      summary: Dashboard identity
      security:
        - dashboardBearer: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Whoami"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Dashboard not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/metrics:
    get:
      tags: [dashboard]
      summary: Dashboard metrics
      security:
        - dashboardBearer: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardMetricsResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "503":
          description: Database not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Dashboard not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/api-keys:
    get:
      tags: [dashboard]
      summary: List API keys
      security:
        - dashboardBearer: []
      parameters:
        - in: query
          name: cursor
          schema:
            type: string
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardAPIKeysListResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Unkey not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    post:
      tags: [dashboard]
      summary: Create API key
      security:
        - dashboardBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DashboardAPIKeyCreateRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardAPIKeyCreateResponse"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "402":
          description: Active subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Unkey not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/api-keys/revoke:
    post:
      tags: [dashboard]
      summary: Revoke API key
      security:
        - dashboardBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DashboardKeyIDRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                    example: true
                required: [ok]
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Unkey not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/api-keys/rotate:
    post:
      tags: [dashboard]
      summary: Rotate API key
      security:
        - dashboardBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DashboardAPIKeyRotateRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardAPIKeyRotateResponse"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Unkey not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/billing/checkout:
    post:
      tags: [dashboard]
      summary: Create Stripe Checkout session
      security:
        - dashboardBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BillingCheckoutRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/BillingCheckoutResponseHosted"
                  - $ref: "#/components/schemas/BillingCheckoutResponseEmbedded"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Billing not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/billing/status:
    get:
      tags: [dashboard]
      summary: Billing status
      security:
        - dashboardBearer: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                    example: true
                  active:
                    type: boolean
                    description: True when the owner has an active entitlement (access_state active).
                  is_active:
                    type: boolean
                    description: Raw entitlements_current.is_active (may be true even if access_state is not active).
                  access_state:
                    type: string
                  customer_id:
                    type: string
                required: [ok, active, access_state, customer_id]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "503":
          description: Database not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Dashboard not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /dashboard/billing/portal:
    post:
      tags: [dashboard]
      summary: Create Stripe billing portal session
      security:
        - dashboardBearer: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingPortalResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "404":
          description: No Stripe customer found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "501":
          description: Billing not configured or dashboard disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
        "503":
          description: Database not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardError"
    options:
      tags: [dashboard]
      summary: CORS preflight
      description: Returns CORS headers for browser preflight requests.
      responses:
        "204":
          description: No Content
        "403":
          description: CORS origin not allowed
  /webhooks/stripe:
    post:
      tags: [stripe]
      summary: Stripe webhook receiver (entitlements)
      description: >
        Only active when `TRAVEL_STRIPE_WEBHOOK_SECRET` is configured.
        If disabled, this path may return 404.
      parameters:
        - in: header
          name: Stripe-Signature
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StripeWebhookResponse"
        "400":
          description: Bad request
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Unauthorized (bad signature)
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Not enabled
          content:
            text/plain:
              schema:
                type: string
  /internal/stripe/webhook-idempotency:
    post:
      tags: [stripe]
      summary: Stripe webhook idempotency service
      description: >
        Internal helper service for webhook idempotency. Only active when
        `TRAVEL_STRIPE_WEBHOOK_IDEMP_SERVICE_ENABLED=true`. If disabled, this path may return 404.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StripeIdempotencyServiceRequest"
      responses:
        "200":
          description: try_mark response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StripeIdempotencyTryMarkResponse"
        "204":
          description: commit/unmark success
        "400":
          description: Bad request
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Unauthorized
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Not enabled
          content:
            text/plain:
              schema:
                type: string
