openapi: 3.1.0
info:
  title: Publica.la API v3 – Content
  version: '3.0.0'
  description: |
    Public content endpoint providing lightweight, cursor-paginated access to store content.
    Authentication is required via API-Key (header `X-User-Token`).
servers:
  - url: https://{store_final_domain}/api/v3
    description: Production

tags:
  - name: Content
    description: List store content items

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-User-Token

  parameters:
    filterQueryParam:
      name: filter[query]
      in: query
      description: Search by title.
      schema:
        type: string
    externalIdParam:
      name: filter[external_id]
      in: query
      description: Exact match search by external_id / ISBN.
      schema:
        type: string
    createdAtFromParam:
      name: filter[created_at][from]
      in: query
      description: 'Filter by creation date from (format: YYYY-MM-DD HH:mm:ss).'
      schema:
        type: string
        format: date-time
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        example: '2020-01-01 00:00:00'
    createdAtToParam:
      name: filter[created_at][to]
      in: query
      description: 'Filter by creation date to (format: YYYY-MM-DD HH:mm:ss).'
      schema:
        type: string
        format: date-time
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        example: '2020-01-31 23:59:59'
    updatedAtFromParam:
      name: filter[updated_at][from]
      in: query
      description: 'Filter by update date from (format: YYYY-MM-DD HH:mm:ss). Maximum 1 month backwards.'
      schema:
        type: string
        format: date-time
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        example: '2024-12-01 00:00:00'
    updatedAtToParam:
      name: filter[updated_at][to]
      in: query
      description: 'Filter by update date to (format: YYYY-MM-DD HH:mm:ss). Cannot be in the future.'
      schema:
        type: string
        format: date-time
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        example: '2024-12-31 23:59:59'
    sortParam:
      name: sort
      in: query
      description: |
        Sort field and direction.
        Available fields: `created_at`, `updated_at`.
        Prefix with `-` for descending order.
      schema:
        type: string
        enum: [created_at, -created_at, updated_at, -updated_at]
    perPageParam:
      name: per_page
      in: query
      description: Page size (1-500, default 100).
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 100
    cursorParam:
      name: cursor
      in: query
      description: Cursor token returned by pagination.
      schema:
        type: string
    includeParam:
      name: include
      in: query
      description: |
        CSV list of additional blocks to include.
        Allowed values: `prices`, `description`, `metadata`, `geographic_restrictions`.
        - `metadata` expands to publisher, author, bisac, keywords and metrics.
        - `geographic_restrictions` provides territorial rights information.
      style: form
      explode: false
      schema:
        type: string
    fieldsParam:
      name: fields
      in: query
      description: CSV list of fields to return after applying *include*.
      style: form
      explode: false
      schema:
        type: string

  schemas:
    Price:
      type: object
      properties:
        currency_id:
          type: string
          minLength: 3
          maxLength: 3
        amount:
          type: number
          format: float
          minimum: 0
      required: [currency_id, amount]

    Content:
      type: object
      properties:
        id: { type: integer }
        external_id: { type: string }
        name: { type: string }
        audience: { type: string, nullable: true }
        slug: { type: string }
        lang: { type: string }
        file_type: { type: string }
        cover_url: { type: string, format: uri }
        reader_url: { type: string, format: uri }
        product_url: { type: string, format: uri }
        created_at:
          { type: string, format: date-time, description: ISO 8601 UTC }
        updated_at:
          { type: string, format: date-time, description: ISO 8601 UTC }
        publication_date:
          { type: string, format: date-time, description: ISO 8601 UTC }
        license: { type: string }
        free:
          type: object
          properties:
            enabled: { type: boolean }
            until: { type: string, format: date-time, nullable: true }
            require_login: { type: boolean }
        preview:
          type: object
          properties:
            enabled: { type: boolean }
            require_login: { type: boolean }
        prices:
          type: array
          items: { $ref: '#/components/schemas/Price' }
        description: { type: string }
        publisher:
          type: array
          description: List of publisher names
          items: { type: string }
        author:
          type: array
          description: List of author names
          items: { type: string }
        bisac:
          type: array
          description: List of BISAC classifications (up to 4)
          items:
            type: object
            properties:
              code: { type: string }
              label:
                {
                  type: string,
                  description: Localized hierarchical path (full path),
                }
        keywords:
          type: array
          description: List of keyword strings for content tagging and discovery
          items: { type: string }
        metrics:
          type: object
          properties:
            total_pages: { type: integer, nullable: true }
            total_words: { type: integer, nullable: true }
            total_seconds: { type: integer, nullable: true }
        geographic_restrictions:
          type: object
          nullable: true
          description: |
            Territorial rights information for the content.
            - `null`: No restrictions, available worldwide
            - Object with both `included` and `excluded` arrays for territorial rights
              - Whitelist: `included` has country codes, `excluded` is empty
              - Blacklist: `included` has ['WORLD'], `excluded` has restricted countries
          properties:
            included:
              type: array
              description: |
                ISO country codes where content IS available.
                Use 'WORLD' to indicate worldwide availability with exclusions.
              items: { type: string }
            excluded:
              type: array
              description: ISO country codes where content is NOT available
              items: { type: string }
          required: [included, excluded]
      required:
        - id
        - name
        - lang
        - file_type
        - cover_url
        - reader_url
        - product_url
        - created_at
        - publication_date

    Links:
      type: object
      properties:
        next: { type: string, format: uri, nullable: true }
        prev: { type: string, format: uri, nullable: true }

    Meta:
      type: object
      properties:
        has_more: { type: boolean }

    ContentList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Content' }
        links: { $ref: '#/components/schemas/Links' }
        meta: { $ref: '#/components/schemas/Meta' }
      required: [data, links, meta]

paths:
  /content:
    get:
      summary: List store content
      description: Returns a cursor-paginated list of content.
      tags: [Content]
      operationId: listContent
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/filterQueryParam'
        - $ref: '#/components/parameters/externalIdParam'
        - $ref: '#/components/parameters/createdAtFromParam'
        - $ref: '#/components/parameters/createdAtToParam'
        - $ref: '#/components/parameters/updatedAtFromParam'
        - $ref: '#/components/parameters/updatedAtToParam'
        - $ref: '#/components/parameters/sortParam'
        - $ref: '#/components/parameters/perPageParam'
        - $ref: '#/components/parameters/cursorParam'
        - $ref: '#/components/parameters/includeParam'
        - $ref: '#/components/parameters/fieldsParam'
        - $ref: '#/components/parameters/updatedAtFromParam'
        - $ref: '#/components/parameters/updatedAtToParam'
        - $ref: '#/components/parameters/createdAtFromParam'
        - $ref: '#/components/parameters/createdAtToParam'

      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentList'
              examples:
                default:
                  summary: Core with pagination
                  value:
                    data:
                      - id: 468166
                        external_id: '9781496822482'
                        name: 'Conversations with Donald Hall'
                        slug: 'conversations-with-donald-hall'
                        lang: 'en'
                        file_type: 'epub'
                        cover_url: 'https://.../468166.jpg'
                        reader_url: 'https://.../read/...'
                        product_url: 'https://.../library/publication/...'
                        created_at: '2021-03-19T00:00:00Z'
                        publication_date: '2019-09-13T00:00:00Z'
                    links:
                      next: 'https://.../content?cursor=abc123&per_page=100'
                      prev: null
                    meta:
                      has_more: true
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Date must be within the last month'
                  errors:
                    type: object
                    example:
                      'filter.updated_at.from':
                        ['Date must be within the last month']
              examples:
                dateRangeExceeded:
                  summary: Date range exceeds 1 month
                  value:
                    message: 'Date range cannot exceed 1 month'
                    errors:
                      'filter.created_at.from':
                        ['Date range cannot exceed 1 month']
                futureDate:
                  summary: Future date not allowed
                  value:
                    message: 'Date cannot be in the future'
                    errors:
                      'filter.updated_at.to': ['Date cannot be in the future']
                invalidRange:
                  summary: Start date after end date
                  value:
                    message: 'Start date must be before end date'
                    errors:
                      'filter.updated_at.from':
                        ['Start date must be before end date']
        '401':
          description: Unauthorized – missing or invalid API-Key
