openapi: 3.0.0
info:
  title: Harmony API
  version: 1.0.0
servers:
  - url: http://localhost/harmonyAPI
    description: Local
  - url: https://api-gateway.radioreformation.com
    description: Production
tags:
  - name: Public API
    description: Public `/v1/*` API endpoints.
  - name: Admin API
    description: Internal `/v1/admin/*` administrative endpoints.
paths:
  /v1/health:
    get:
      tags:
        - Public API
      summary: Health check
      description: Returns Harmony service metadata, a derived runtime status summary, dependency checks, and safe configuration details for operational inspection.
      operationId: getHealth
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Healthy response
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestIdHeader'
            X-Content-Type-Options:
              $ref: '#/components/headers/ContentTypeOptionsHeader'
            Cache-Control:
              $ref: '#/components/headers/CacheControlHeader'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimitHeader'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemainingHeader'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitExceededError'
  /v1/podcasts/feeds/fetch:
    post:
      tags:
        - Public API
      summary: Fetch remote podcast feed
      description: Fetches a remote podcast feed, normalizes RSS, iTunes, and Podcast 2.0 metadata into Harmony's podcast structure, and returns remote metadata needed for future sync and storage decisions.
      operationId: fetchRemotePodcastFeed
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PodcastFeedFetchRequest'
            examples:
              default:
                value:
                  feed_url: https://example.com/feed.xml
                  probe_media_assets: true
                  probe_media_assets_limit: 5
      responses:
        '200':
          description: Remote podcast feed fetched and normalized successfully
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestIdHeader'
            X-Content-Type-Options:
              $ref: '#/components/headers/ContentTypeOptionsHeader'
            Cache-Control:
              $ref: '#/components/headers/CacheControlHeader'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimitHeader'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemainingHeader'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PodcastFeedFetchResponse'
              examples:
                default:
                  value:
                    request:
                      feed_url: https://example.com/feed.xml
                      status: ok
                      content_validated: true
                    remote:
                      feed:
                        requested_url: https://example.com/feed.xml
                        effective_url: https://cdn.example.com/feed.xml
                        status_code: 200
                        content_type: application/rss+xml
                        content_length: 123456
                        decoded_content_length: 234567
                        redirect_count: 1
                        etag: '"feed-etag-123"'
                        last_modified: Wed, 19 Mar 2026 00:00:00 GMT
                        headers:
                          content-type:
                            - application/rss+xml
                          etag:
                            - '"feed-etag-123"'
                        feed_sha256: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
                        xml_warning_count: 0
                        xml_repaired: false
                      images:
                        - image_url: https://example.com/artwork.jpg
                          status_code: 200
                          content_type: image/jpeg
                          etag: '"image-etag-123"'
                          last_modified: Wed, 19 Mar 2026 00:00:00 GMT
                          error: null
                    podcast:
                      title: Example Podcast
                      subtitle: Weekly stories
                      description_html: <p>Example description</p>
                      description_text: Example description
                      language: en-us
                      feed_url: https://cdn.example.com/feed.xml
                      link_url: https://example.com
                      artwork_url: https://example.com/artwork.jpg
                      explicit: 0
                      owner_name: Example Publisher
                      owner_email: feed@example.com
                      type: episodic
                      summary: Example summary
                      pub_date: '2026-03-18T00:00:00Z'
                      last_build_date: '2026-03-19T00:00:00Z'
                      updateFrequency:
                        minutes: 1440
                        isoDuration: P1D
                        rrule: FREQ=DAILY
                        complete: false
                        source: undefined
                        dtstart: null
                        label: null
                    podcast_categories:
                      - slug: news
                        display_name: News
                        source: rss
                    podcast_images:
                      - source: rss
                        kind: primary
                        image_url: https://example.com/artwork.jpg
                        width_px: 3000
                        height_px: 3000
                        checked_width_px: 3000
                        checked_height_px: 3000
                    episodes:
                      - id: ep-1
                        guid: ep-1
                        guid_is_permalink: 0
                        link_url: https://example.com/episodes/1
                        title: Episode One
                        subtitle: First episode
                        description_html: <p>Episode description</p>
                        description_text: Episode description
                        summary_text: Episode summary
                        published_at: '2026-03-18T00:00:00Z'
                        duration_seconds: 3600
                        explicit: 0
                        episode_type: full
                        season_number: 1
                        episode_number: 1
                        episode_images: []
                        media_assets:
                          - kind: primary
                            delivery: download
                            url: https://example.com/audio.mp3
                            mime_type: audio/mpeg
                            media_category: audio
                            file_extension: mp3
                            length_bytes: 12345
                            bitrate_kbps: null
                            duration_seconds: 3600
                            integrity_etag: null
                            checksum_sha256: null
                            checked_effective_url: https://cdn.example.com/audio.mp3
                            checked_status_code: 200
                            checked_content_type: audio/mpeg
                            checked_content_length: 12345
                            checked_redirect_count: 1
                            checked_etag: '"audio-etag-123"'
                            checked_last_modified: Wed, 19 Mar 2026 00:00:00 GMT
                            probe_error: null
                    unhandled_tags:
                      channel: []
                      items: []
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '422':
          description: Invalid request payload or fetched content was not a supported podcast feed
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestIdHeader'
            X-Content-Type-Options:
              $ref: '#/components/headers/ContentTypeOptionsHeader'
            Cache-Control:
              $ref: '#/components/headers/CacheControlHeader'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidationErrorResponse'
                  - $ref: '#/components/schemas/PodcastFeedFetchErrorResponse'
              examples:
                validation_failed:
                  value:
                    message: The given data was invalid.
                    code: validation_failed
                    request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                    errors:
                      feed_url:
                        - The feed_url field is required.
                invalid_podcast_feed:
                  value:
                    message: The remote feed could not be normalized.
                    code: invalid_podcast_feed
                    request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                    request:
                      feed_url: https://example.com/not-a-podcast.xml
                      status: error
                      content_validated: true
                      error_message: Podcast feed is missing <channel> root node.
                    remote:
                      feed:
                        requested_url: https://example.com/not-a-podcast.xml
                        effective_url: https://example.com/not-a-podcast.xml
                        status_code: 200
                        content_type: application/rss+xml
                        content_length: 128
                        decoded_content_length: 128
                        redirect_count: 0
                        etag: null
                        last_modified: null
                        headers: {}
                        feed_sha256: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
                        xml_warning_count: 0
                        xml_repaired: false
                        error: Missing channel node
                      images: []
        '429':
          $ref: '#/components/responses/RateLimitExceededError'
        '502':
          description: Remote feed acquisition failed
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestIdHeader'
            X-Content-Type-Options:
              $ref: '#/components/headers/ContentTypeOptionsHeader'
            Cache-Control:
              $ref: '#/components/headers/CacheControlHeader'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PodcastFeedFetchErrorResponse'
              examples:
                upstream_fetch_failed:
                  value:
                    message: The remote feed could not be fetched.
                    code: remote_feed_fetch_failed
                    request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                    request:
                      feed_url: https://blocked.example/feed.xml
                      status: error
                      content_validated: false
                      error_message: DNS resolution failed
                    remote:
                      feed:
                        requested_url: https://blocked.example/feed.xml
                        effective_url: null
                        status_code: null
                        content_type: null
                        content_length: null
                        decoded_content_length: null
                        redirect_count: 0
                        etag: null
                        last_modified: null
                        headers: {}
                        feed_sha256: null
                        xml_warning_count: 0
                        xml_repaired: false
                        error: DNS resolution failed
                      images: []
  /v1/admin/keys:
    get:
      tags:
        - Admin API
      summary: List API keys
      description: Returns a bounded JSON list of API keys for administrative inspection.
      operationId: listAdminApiKeys
      security:
        - ApiKeyAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: API keys returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListResponse'
              examples:
                default:
                  value:
                    data:
                      - public_id: ak_existingkey00000000000001
                        tenant:
                          public_id: tn_alpha0000000000000000000001
                          slug: alpha
                          name: Alpha Tenant
                        name: Existing Key
                        key_class: server
                        rate_limit_strategy: api_key
                        rate_limit_per_minute: 600
                        scopes:
                          - health:read
                        is_active: true
                        expires_at: null
                        last_used_at: null
                        revoked_at: null
                        created_at: '2026-03-19T00:00:00+00:00'
                        updated_at: '2026-03-19T00:00:00+00:00'
                    meta:
                      count: 1
                      limit: 50
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenScopeError'
        '422':
          $ref: '#/components/responses/ValidationError'
    post:
      tags:
        - Admin API
      summary: Create API key
      description: Creates a database-managed API key, stores only the hash, and returns the plaintext key once in the response.
      operationId: createAdminApiKey
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyStoreRequest'
            examples:
              default:
                value:
                  tenant_public_id: tn_alpha0000000000000000000001
                  name: Admin Automation Key
                  key_class: admin
                  rate_limit_strategy: api_key
                  rate_limit_per_minute: 120
                  expires_at: '2030-01-01T00:00:00Z'
                  scopes:
                    - admin:api_keys
                    - health:read
      responses:
        '201':
          description: API key created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyStoreResponse'
              examples:
                default:
                  value:
                    data:
                      api_key:
                        public_id: ak_01hf7v2h9q0x4t1m6p3s8n5k7z
                        tenant:
                          public_id: tn_alpha0000000000000000000001
                          slug: alpha
                          name: Alpha Tenant
                        name: Admin Automation Key
                        key_class: admin
                        rate_limit_strategy: api_key
                        rate_limit_per_minute: 120
                        scopes:
                          - admin:api_keys
                          - health:read
                        is_active: true
                        expires_at: '2030-01-01T00:00:00+00:00'
                        last_used_at: null
                        revoked_at: null
                        created_at: '2026-03-19T00:10:00+00:00'
                        updated_at: '2026-03-19T00:10:00+00:00'
                      one_time_key: hmy_0123456789abcdef0123456789abcdef01234567
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenScopeError'
        '422':
          $ref: '#/components/responses/ValidationError'
  /v1/admin/keys/create:
    get:
      tags:
        - Admin API
      summary: Get key creation metadata
      description: Returns the active tenants and accepted field shape for the API key creation endpoint.
      operationId: getAdminApiKeyCreateMetadata
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Key creation metadata returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreateMetadataResponse'
              examples:
                default:
                  value:
                    data:
                      tenants:
                        - public_id: tn_alpha0000000000000000000001
                          slug: alpha
                          name: Alpha Tenant
                      fields:
                        tenant_public_id:
                          required: true
                          type: string
                        name:
                          required: true
                          type: string
                        key_class:
                          required: false
                          type: string
                          allowed:
                            - public_site
                            - server
                            - admin
                            - internal
                          default: server
                        rate_limit_strategy:
                          required: false
                          type: string
                          allowed:
                            - api_key
                            - api_key_ip
                          default: api_key
                        rate_limit_per_minute:
                          required: false
                          type: integer
                          minimum: 1
                          default: 600
                        expires_at:
                          required: false
                          type: date-time
                        scopes:
                          required: false
                          type: array
                      reserved_scopes:
                        - admin:api_keys
                      policy:
                        key_classes:
                          - public_site
                          - server
                          - admin
                          - internal
                        rate_limit_strategies:
                          - api_key
                          - api_key_ip
                        defaults:
                          key_class: server
                          rate_limit_strategy: api_key
                          rate_limit_per_minute: 600
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalid_api_key:
                  value:
                    message: The provided API key is invalid.
                    code: invalid_api_key
                    request_id: 101129b9-8776-4154-b866-fab4b18a4a18
        '403':
          description: API key does not have the required admin scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopeErrorResponse'
              examples:
                insufficient_scope:
                  value:
                    message: The API key is not authorized for this action.
                    code: insufficient_scope
                    request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                    required_scope: admin:api_keys
  /v1/admin/keys/{public_id}:
    get:
      tags:
        - Admin API
      summary: Get API key detail
      description: Returns one API key record with tenant, scope, status, and lifecycle fields.
      operationId: showAdminApiKey
      security:
        - ApiKeyAuth: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyDetailResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenScopeError'
        '404':
          $ref: '#/components/responses/ApiKeyNotFoundError'
    patch:
      tags:
        - Admin API
      summary: Update API key
      description: Updates an API key's name, scopes, expiry, and rate-limit policy fields without rotating the one-time secret value.
      operationId: updateAdminApiKey
      security:
        - ApiKeyAuth: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyUpdateRequest'
            examples:
              default:
                value:
                  name: Website Key
                  key_class: public_site
                  rate_limit_per_minute: 180
                  scopes:
                    - health:read
      responses:
        '200':
          description: API key updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyDetailResponse'
              examples:
                default:
                  value:
                    data:
                      public_id: ak_existingkey00000000000001
                      tenant:
                        public_id: tn_alpha0000000000000000000001
                        slug: alpha
                        name: Alpha Tenant
                      name: Website Key
                      key_class: public_site
                      rate_limit_strategy: api_key_ip
                      rate_limit_per_minute: 180
                      scopes:
                        - health:read
                      is_active: true
                      expires_at: null
                      last_used_at: null
                      revoked_at: null
                      created_at: '2026-03-19T00:00:00+00:00'
                      updated_at: '2026-03-20T00:00:00+00:00'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenScopeError'
        '404':
          $ref: '#/components/responses/ApiKeyNotFoundError'
        '422':
          $ref: '#/components/responses/ValidationError'
  /v1/admin/keys/{public_id}/revoke:
    post:
      tags:
        - Admin API
      summary: Revoke API key
      description: Revokes an API key by marking it inactive and setting its revocation timestamp.
      operationId: revokeAdminApiKey
      security:
        - ApiKeyAuth: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key revoked successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyDetailResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenScopeError'
        '404':
          $ref: '#/components/responses/ApiKeyNotFoundError'
components:
  headers:
    RequestIdHeader:
      description: Correlation identifier for the request.
      schema:
        type: string
        example: 101129b9-8776-4154-b866-fab4b18a4a18
    ContentTypeOptionsHeader:
      description: Prevents MIME type sniffing.
      schema:
        type: string
        example: nosniff
    CacheControlHeader:
      description: Prevents intermediary and client-side storage of response bodies.
      schema:
        type: string
        example: no-store, private
    RateLimitLimitHeader:
      description: Rate-limit ceiling for the current window.
      schema:
        type: integer
        example: 60
    RateLimitRemainingHeader:
      description: Remaining requests for the current window.
      schema:
        type: integer
        example: 58
    RetryAfterHeader:
      description: Seconds until the client should retry after throttling.
      schema:
        type: integer
        example: 60
    RateLimitResetHeader:
      description: Seconds until the current rate-limit window resets.
      schema:
        type: integer
        example: 60
  responses:
    UnauthorizedError:
      description: Missing or invalid API key
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestIdHeader'
        X-Content-Type-Options:
          $ref: '#/components/headers/ContentTypeOptionsHeader'
        Cache-Control:
          $ref: '#/components/headers/CacheControlHeader'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missing_api_key:
              value:
                message: The x-api-key header is required.
                code: missing_api_key
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
            invalid_api_key:
              value:
                message: The provided API key is invalid.
                code: invalid_api_key
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
    ForbiddenScopeError:
      description: API key does not have the required admin scope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScopeErrorResponse'
          examples:
            insufficient_scope:
              value:
                message: The API key is not authorized for this action.
                code: insufficient_scope
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                required_scope: admin:api_keys
    ValidationError:
      description: Request data was invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
          examples:
            validation_failed:
              value:
                message: The given data was invalid.
                code: validation_failed
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
                errors:
                  field:
                    - The field is invalid.
    RateLimitExceededError:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfterHeader'
        X-Request-Id:
          $ref: '#/components/headers/RequestIdHeader'
        X-Content-Type-Options:
          $ref: '#/components/headers/ContentTypeOptionsHeader'
        Cache-Control:
          $ref: '#/components/headers/CacheControlHeader'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimitHeader'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemainingHeader'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitResetHeader'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            rate_limited:
              value:
                message: Too Many Attempts.
                code: too_many_requests
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
    ApiKeyNotFoundError:
      description: API key not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            not_found:
              value:
                message: The requested API key could not be found.
                code: not_found
                request_id: 101129b9-8776-4154-b866-fab4b18a4a18
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    HealthResponse:
      type: object
      required:
        - status
        - status_summary
        - service
        - version
        - timestamp
        - checks
        - config
      properties:
        status:
          type: string
          example: ok
        status_summary:
          type: string
          example: ok
        service:
          type: string
          example: Harmony API Gateway
        version:
          type: string
          example: 1.0.0
        timestamp:
          type: string
          format: date-time
        checks:
          type: object
          required:
            - database
            - redis
            - queue
            - cache
            - storage
            - app_key
            - bootstrap_api_keys
          properties:
            database:
              $ref: '#/components/schemas/DependencyCheck'
            redis:
              $ref: '#/components/schemas/RedisCheck'
            queue:
              $ref: '#/components/schemas/QueueCheck'
            cache:
              $ref: '#/components/schemas/CacheCheck'
            storage:
              $ref: '#/components/schemas/StorageCheck'
            app_key:
              $ref: '#/components/schemas/AppKeyCheck'
            bootstrap_api_keys:
              $ref: '#/components/schemas/BootstrapApiKeysCheck'
        config:
          $ref: '#/components/schemas/HealthConfig'
    DependencyCheck:
      type: object
      required:
        - status
        - connection
      properties:
        status:
          type: string
          example: connected
        driver:
          type: string
          nullable: true
          example: mysql
        connection:
          type: string
          nullable: true
          example: mysql
        error:
          type: string
          nullable: true
          example: ConnectionException
    RedisCheck:
      type: object
      required:
        - status
        - connection
      properties:
        status:
          type: string
          example: connected
        client:
          type: string
          nullable: true
          example: phpredis
        connection:
          type: string
          nullable: true
          example: default
        error:
          type: string
          nullable: true
          example: RedisException
    QueueCheck:
      type: object
      required:
        - status
        - connection
      properties:
        status:
          type: string
          example: ready
        driver:
          type: string
          nullable: true
          example: redis
        connection:
          type: string
          nullable: true
          example: redis
        backend_connection:
          type: string
          nullable: true
          example: default
    CacheCheck:
      type: object
      required:
        - status
        - store
      properties:
        status:
          type: string
          example: ready
        store:
          type: string
          nullable: true
          example: redis
        driver:
          type: string
          nullable: true
          example: redis
        error:
          type: string
          nullable: true
          example: RedisException
    StorageCheck:
      type: object
      required:
        - status
        - paths
      properties:
        status:
          type: string
          example: ready
        paths:
          type: object
          required:
            - storage
            - bootstrap_cache
          properties:
            storage:
              $ref: '#/components/schemas/StoragePathCheck'
            bootstrap_cache:
              $ref: '#/components/schemas/StoragePathCheck'
    StoragePathCheck:
      type: object
      required:
        - path
        - status
      properties:
        path:
          type: string
          example: /path/to/storage
        status:
          type: string
          example: writable
    AppKeyCheck:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          example: configured
    BootstrapApiKeysCheck:
      type: object
      required:
        - status
        - count
      properties:
        status:
          type: string
          example: configured
        count:
          type: integer
          example: 1
    HealthConfig:
      type: object
      required:
        - environment
        - debug
        - app_url
        - api_prefix
        - cache_store
        - queue_connection
        - log_channel
        - jwt_guard
        - rate_limit_per_minute
      properties:
        environment:
          type: string
          example: production
        debug:
          type: boolean
          example: false
        app_url:
          type: string
          nullable: true
          example: https://api-gateway.radioreformation.com
        api_prefix:
          type: string
          example: /v1
        cache_store:
          type: string
          nullable: true
          example: file
        queue_connection:
          type: string
          nullable: true
          example: redis
        log_channel:
          type: string
          nullable: true
          example: api
        jwt_guard:
          type: string
          nullable: true
          example: api
        rate_limit_per_minute:
          type: integer
          example: 60
    PodcastFeedFetchRequest:
      type: object
      required:
        - feed_url
      properties:
        feed_url:
          type: string
          format: uri
          example: https://example.com/feed.xml
        probe_media_assets:
          type: boolean
          description: When true, probe each episode media asset for resolved URL, response headers, and length metadata.
          default: false
        probe_media_assets_limit:
          type: integer
          nullable: true
          minimum: 1
          maximum: 250
          description: Limits media-asset probing to the first N episodes in the returned feed order. Ignored unless probe_media_assets is true.
          example: 5
    PodcastFeedFetchResponse:
      type: object
      required:
        - request
        - remote
        - podcast
        - podcast_categories
        - podcast_images
        - episodes
        - unhandled_tags
      properties:
        request:
          $ref: '#/components/schemas/PodcastFeedRequestInfo'
        remote:
          $ref: '#/components/schemas/PodcastFeedRemote'
        podcast:
          $ref: '#/components/schemas/PodcastResource'
        podcast_categories:
          type: array
          items:
            $ref: '#/components/schemas/PodcastCategory'
        podcast_images:
          type: array
          items:
            $ref: '#/components/schemas/PodcastImage'
        episodes:
          type: array
          items:
            $ref: '#/components/schemas/PodcastEpisode'
        unhandled_tags:
          $ref: '#/components/schemas/UnhandledTags'
    PodcastFeedFetchErrorResponse:
      type: object
      required:
        - message
        - code
        - request_id
        - request
        - remote
      properties:
        message:
          type: string
          example: The remote feed could not be fetched.
        code:
          type: string
          example: remote_feed_fetch_failed
        request_id:
          type: string
        request:
          $ref: '#/components/schemas/PodcastFeedRequestInfo'
        remote:
          $ref: '#/components/schemas/PodcastFeedRemote'
    PodcastFeedRequestInfo:
      type: object
      required:
        - feed_url
        - status
        - content_validated
      properties:
        feed_url:
          type: string
          nullable: true
          format: uri
        status:
          type: string
          example: ok
        content_validated:
          type: boolean
          nullable: true
        error_message:
          type: string
          nullable: true
    PodcastFeedRemote:
      type: object
      required:
        - feed
        - images
      properties:
        feed:
          $ref: '#/components/schemas/RemoteFeedMeta'
        images:
          type: array
          items:
            $ref: '#/components/schemas/RemoteImageProbe'
    RemoteFeedMeta:
      type: object
      required:
        - requested_url
        - effective_url
        - status_code
        - content_type
        - content_length
        - redirect_count
        - etag
        - last_modified
        - headers
        - feed_sha256
        - xml_warning_count
        - xml_repaired
      properties:
        requested_url:
          type: string
          nullable: true
        effective_url:
          type: string
          nullable: true
        status_code:
          type: integer
          nullable: true
          example: 200
        content_type:
          type: string
          nullable: true
          example: application/rss+xml
        content_length:
          type: integer
          nullable: true
          description: Response Content-Length header value when provided by the upstream server.
          example: 123456
        decoded_content_length:
          type: integer
          nullable: true
          description: Size in bytes of the decoded response body that Harmony parsed.
          example: 234567
        redirect_count:
          type: integer
          example: 1
        etag:
          type: string
          nullable: true
        last_modified:
          type: string
          nullable: true
        headers:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        feed_sha256:
          type: string
          nullable: true
        xml_warning_count:
          type: integer
          example: 0
        xml_repaired:
          type: boolean
        error:
          type: string
          nullable: true
    RemoteImageProbe:
      type: object
      required:
        - image_url
        - status_code
        - content_type
        - etag
        - last_modified
        - error
      properties:
        image_url:
          type: string
        status_code:
          type: integer
          nullable: true
        content_type:
          type: string
          nullable: true
        etag:
          type: string
          nullable: true
        last_modified:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
    PodcastResource:
      type: object
      required:
        - title
        - description_html
        - description_text
        - language
        - feed_url
        - link_url
        - artwork_url
        - explicit
        - owner_name
        - owner_email
        - type
        - summary
        - pub_date
        - last_build_date
        - updateFrequency
      properties:
        title:
          type: string
        subtitle:
          type: string
          nullable: true
        copyright:
          type: string
          nullable: true
        description_html:
          type: string
        description_text:
          type: string
        language:
          type: string
          nullable: true
        feed_url:
          type: string
        new_feed_url:
          type: string
          nullable: true
        link_url:
          type: string
        artwork_url:
          type: string
          nullable: true
        explicit:
          type: integer
          example: 0
        country:
          type: string
          nullable: true
        itunes_author:
          type: string
          nullable: true
        owner_name:
          type: string
          nullable: true
        owner_email:
          type: string
          nullable: true
        type:
          type: string
          example: episodic
        generator:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        keywords:
          type: array
          nullable: true
          items:
            type: string
        pub_date:
          type: string
          nullable: true
          format: date-time
        last_build_date:
          type: string
          nullable: true
          format: date-time
        channel_guid:
          type: string
          nullable: true
        locked:
          type: integer
          nullable: true
        locked_owner_email:
          type: string
          nullable: true
        license_name:
          type: string
          nullable: true
        license_url:
          type: string
          nullable: true
        last_modified:
          type: string
          nullable: true
        hmny_xstore_managing_editor:
          type: object
          nullable: true
        updateFrequency:
          $ref: '#/components/schemas/PodcastUpdateFrequency'
    PodcastUpdateFrequency:
      type: object
      required:
        - minutes
        - isoDuration
        - rrule
        - complete
        - source
        - dtstart
        - label
      properties:
        minutes:
          type: integer
          nullable: true
        isoDuration:
          type: string
          nullable: true
        rrule:
          type: string
          nullable: true
        complete:
          type: boolean
        source:
          type: string
        dtstart:
          type: string
          nullable: true
        label:
          type: string
          nullable: true
    PodcastCategory:
      type: object
      required:
        - slug
        - display_name
        - source
      properties:
        slug:
          type: string
        display_name:
          type: string
        source:
          type: string
    PodcastImage:
      type: object
      required:
        - source
        - kind
        - image_url
      properties:
        source:
          type: string
        kind:
          type: string
        image_url:
          type: string
        purpose:
          type: string
          nullable: true
        mime_type:
          type: string
          nullable: true
        alt_text:
          type: string
          nullable: true
        aspect_ratio:
          type: string
          nullable: true
        width_px:
          type: integer
          nullable: true
        height_px:
          type: integer
          nullable: true
        file_size_bytes:
          type: integer
          nullable: true
        link_url:
          type: string
          nullable: true
        checked_width_px:
          type: integer
          nullable: true
        checked_height_px:
          type: integer
          nullable: true
        hmny_xstore_checked_content_type:
          type: string
          nullable: true
        hmny_xstore_image_error:
          type: string
          nullable: true
    PodcastEpisode:
      type: object
      required:
        - id
        - guid
        - guid_is_permalink
        - link_url
        - title
        - description_html
        - description_text
        - published_at
        - duration_seconds
        - explicit
        - episode_type
        - episode_images
        - media_assets
      properties:
        id:
          type: string
        guid:
          type: string
        guid_is_permalink:
          type: integer
        link_url:
          type: string
        title:
          type: string
        subtitle:
          type: string
          nullable: true
        description_html:
          type: string
        description_text:
          type: string
        summary_text:
          type: string
          nullable: true
        published_at:
          type: string
          nullable: true
          format: date-time
        duration_seconds:
          type: integer
          nullable: true
        explicit:
          type: integer
        episode_type:
          type: string
        season_number:
          type: integer
          nullable: true
        episode_number:
          type: integer
          nullable: true
        episode_images:
          type: array
          items:
            $ref: '#/components/schemas/PodcastImage'
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/PodcastMediaAsset'
    PodcastMediaAsset:
      type: object
      required:
        - kind
        - delivery
        - url
        - mime_type
        - media_category
        - file_extension
        - length_bytes
        - bitrate_kbps
        - duration_seconds
        - integrity_etag
        - checksum_sha256
      properties:
        kind:
          type: string
        delivery:
          type: string
        url:
          type: string
        mime_type:
          type: string
          nullable: true
        media_category:
          type: string
        file_extension:
          type: string
          nullable: true
        length_bytes:
          type: integer
          nullable: true
        bitrate_kbps:
          type: integer
          nullable: true
        duration_seconds:
          type: integer
          nullable: true
        integrity_etag:
          type: string
          nullable: true
        checksum_sha256:
          type: string
          nullable: true
        checked_effective_url:
          type: string
          nullable: true
        checked_status_code:
          type: integer
          nullable: true
        checked_content_type:
          type: string
          nullable: true
        checked_content_length:
          type: integer
          nullable: true
        checked_redirect_count:
          type: integer
          nullable: true
        checked_etag:
          type: string
          nullable: true
        checked_last_modified:
          type: string
          nullable: true
        probe_error:
          type: string
          nullable: true
    UnhandledTags:
      type: object
      required:
        - channel
        - items
      properties:
        channel:
          type: array
          items:
            $ref: '#/components/schemas/UnhandledTag'
        items:
          type: array
          items:
            $ref: '#/components/schemas/UnhandledTag'
    UnhandledTag:
      type: object
      required:
        - namespace
        - tag
        - count
      properties:
        namespace:
          type: string
          nullable: true
        tag:
          type: string
        count:
          type: integer
    TenantOption:
      type: object
      required:
        - public_id
        - slug
        - name
      properties:
        public_id:
          type: string
          example: tn_alpha0000000000000000000001
        slug:
          type: string
          example: alpha
        name:
          type: string
          example: Alpha Tenant
    ApiKeyResource:
      type: object
      required:
        - public_id
        - tenant
        - name
        - key_class
        - rate_limit_strategy
        - rate_limit_per_minute
        - scopes
        - is_active
        - expires_at
        - last_used_at
        - revoked_at
        - created_at
        - updated_at
      properties:
        public_id:
          type: string
          example: ak_existingkey00000000000001
        tenant:
          $ref: '#/components/schemas/TenantOption'
        name:
          type: string
          example: Existing Key
        key_class:
          type: string
          enum:
            - public_site
            - server
            - admin
            - internal
          example: server
        rate_limit_strategy:
          type: string
          enum:
            - api_key
            - api_key_ip
          example: api_key
        rate_limit_per_minute:
          type: integer
          minimum: 1
          example: 600
        scopes:
          type: array
          items:
            type: string
          example:
            - health:read
        is_active:
          type: boolean
          example: true
        expires_at:
          type: string
          nullable: true
          format: date-time
        last_used_at:
          type: string
          nullable: true
          format: date-time
        revoked_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          nullable: true
          format: date-time
        updated_at:
          type: string
          nullable: true
          format: date-time
    ApiKeyListResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyResource'
        meta:
          type: object
          required:
            - count
            - limit
          properties:
            count:
              type: integer
              example: 2
            limit:
              type: integer
              example: 50
    ApiKeyCreateMetadataResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          required:
            - tenants
            - fields
            - reserved_scopes
            - policy
          properties:
            tenants:
              type: array
              items:
                $ref: '#/components/schemas/TenantOption'
            fields:
              type: object
            reserved_scopes:
              type: array
              items:
                type: string
            policy:
              type: object
    ApiKeyStoreRequest:
      type: object
      required:
        - tenant_public_id
        - name
      properties:
        tenant_public_id:
          type: string
          example: tn_alpha0000000000000000000001
        name:
          type: string
          example: Admin Automation Key
        key_class:
          type: string
          enum:
            - public_site
            - server
            - admin
            - internal
          example: admin
        rate_limit_strategy:
          type: string
          enum:
            - api_key
            - api_key_ip
          example: api_key
        rate_limit_per_minute:
          type: integer
          minimum: 1
          example: 120
        expires_at:
          type: string
          nullable: true
          format: date-time
        scopes:
          type: array
          items:
            type: string
    ApiKeyUpdateRequest:
      type: object
      properties:
        name:
          type: string
          example: Website Key
        key_class:
          type: string
          enum:
            - public_site
            - server
            - admin
            - internal
          example: public_site
        rate_limit_strategy:
          type: string
          enum:
            - api_key
            - api_key_ip
          example: api_key_ip
        rate_limit_per_minute:
          type: integer
          minimum: 1
          example: 180
        expires_at:
          type: string
          nullable: true
          format: date-time
        scopes:
          type: array
          items:
            type: string
    ApiKeyStoreResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          required:
            - api_key
            - one_time_key
          properties:
            api_key:
              $ref: '#/components/schemas/ApiKeyResource'
            one_time_key:
              type: string
              example: hmy_0123456789abcdef0123456789abcdef01234567
              description: One-time secret API key value returned only when the key is created. Store this value securely because it cannot be retrieved again.
    ApiKeyDetailResponse:
      type: object
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/ApiKeyResource'
    ErrorResponse:
      type: object
      required:
        - message
        - code
        - request_id
      properties:
        message:
          type: string
        code:
          type: string
        request_id:
          type: string
    ScopeErrorResponse:
      type: object
      required:
        - message
        - code
        - required_scope
        - request_id
      properties:
        message:
          type: string
        code:
          type: string
        required_scope:
          type: string
          example: admin:api_keys
        request_id:
          type: string
    ValidationErrorResponse:
      type: object
      required:
        - message
        - code
        - request_id
        - errors
      properties:
        message:
          type: string
        code:
          type: string
        request_id:
          type: string
        errors:
          type: object
