openapi: 3.0.3
info:
  title: "Export API"
  description: |
    Export API allows System Integrators to query and filter exported product data.

    ## Authentication

    All requests (except `/`) require the `X-EA-Auth-Token` header.
    Providing this header on the index endpoint has no effect.

    The token must consist only of alphanumeric characters, hyphens (`-`), and underscores (`_`).
    Any other characters result in a `401` with the message `Provided token format is invalid`.

    **v2 routes** (`/sites/{siteId}/channels/{channelId}/destinations/{destinationId}/products`):
    - Destination ID is part of the URL path — no extra header needed.

    **Legacy routes** (`/{siteId}/{channelId}`):
    - Destination ID must be provided via the `X-Destination-Id` header.

    ## Response Formats

    Two response formats are supported, selected via the `Accept` header:

    | Accept header | Format |
    |---|---|
    | `application/vnd.api+json` | JSON:API envelope `{"data": [...], "meta": {...}}` |
    | `application/json` (or omitted) | Legacy raw array `[...]` |

    ## Error Responses

    - `401 Unauthorized`: Authentication token is missing or invalid, or destination ID is missing
    - `403 Forbidden`: Token is not authorized for the specified destination
    - `404 Not Found`: No exported data found or invalid route

  version: "1.0.0"
  contact:
    name: Productsup Engineering
    email: engineering@productsup.com

servers:
  - url: https://export-api.productsup.com
    description: Production environment

security:
  - ApiKeyAuth: []

paths:
  /:
    get:
      summary: Index endpoint
      description: Returns a welcome message. No authentication required. Providing `X-EA-Auth-Token` has no effect.
      security: []
      responses:
        '200':
          description: Welcome message
          content:
            text/html:
              schema:
                type: string
                example: "Welcome to export-api!"

  /sites/{siteId}/channels/{channelId}/destinations/{destinationId}/products:
    parameters:
      - $ref: '#/components/parameters/siteId'
      - $ref: '#/components/parameters/channelId'
      - $ref: '#/components/parameters/destinationId'
      - $ref: '#/components/parameters/limitQuery'
      - $ref: '#/components/parameters/offsetQuery'
      - $ref: '#/components/parameters/orderByQuery'
      - $ref: '#/components/parameters/groupByQuery'
      - $ref: '#/components/parameters/modeQuery'
      - $ref: '#/components/parameters/filterQuery'
    get:
      summary: Get all product data (v2 — recommended)
      description: |
        Retrieves all product data for the specified site, channel, and destination.
        Destination ID is part of the URL — no `X-Destination-Id` header required.

        **Authentication required**: `X-EA-Auth-Token` header.

        **Response format**: controlled by the `Accept` header.
        - `Accept: application/vnd.api+json` → JSON:API envelope with `data` and `meta`
        - `Accept: application/json` (or omitted) → legacy raw array
      responses:
        '200':
          description: Products fetched successfully
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/JsonApiProductListResponse'
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProductResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'

  /{siteId}/{channelId}:
    parameters:
      - $ref: '#/components/parameters/siteId'
      - $ref: '#/components/parameters/channelId'
      - $ref: '#/components/parameters/limitQuery'
      - $ref: '#/components/parameters/offsetQuery'
      - $ref: '#/components/parameters/orderByQuery'
      - $ref: '#/components/parameters/groupByQuery'
      - $ref: '#/components/parameters/modeQuery'
      - $ref: '#/components/parameters/filterQuery'
    get:
      deprecated: true
      summary: Get all product data (legacy)
      description: |
        **Deprecated** — use `/sites/{siteId}/channels/{channelId}/destinations/{destinationId}/products` instead.

        **Authentication required**: `X-EA-Auth-Token` and `X-Destination-Id` headers.
      security:
        - ApiKeyAuth: []
          DestinationId: []
      responses:
        '200':
          description: Products fetched successfully
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/JsonApiProductListResponse'
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProductResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'

  /{siteId}/{channelId}/{column}/{value}:
    parameters:
      - $ref: '#/components/parameters/siteId'
      - $ref: '#/components/parameters/channelId'
      - $ref: '#/components/parameters/limitQuery'
      - $ref: '#/components/parameters/offsetQuery'
      - $ref: '#/components/parameters/orderByQuery'
      - $ref: '#/components/parameters/groupByQuery'
      - name: column
        in: path
        description: Column name to filter on
        required: true
        schema:
          type: string
        example: "color"
      - name: value
        in: path
        description: |
          A single string that may contain comma-separated values.
          The server splits on commas and combines the values with OR (equivalent to an SQL `IN` clause).
        required: true
        schema:
          type: string
        example: "blue,red,green"
    get:
      deprecated: true
      summary: Get filtered product data (legacy)
      description: |
        **Deprecated** — use `/sites/{siteId}/channels/{channelId}/destinations/{destinationId}/products`
        with the `filters` query parameter instead.

        **Authentication required**: `X-EA-Auth-Token` and `X-Destination-Id` headers.
      security:
        - ApiKeyAuth: []
          DestinationId: []
      responses:
        '200':
          description: Filtered products fetched successfully
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/JsonApiProductListResponse'
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProductResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      name: X-EA-Auth-Token
      in: header
      description: |
        Authentication token configured in the destination settings.
        Must match `api_system_integrator_auth_token` value in `pds_destination_settings` table.
        Token must consist only of alphanumeric characters, hyphens (`-`), and underscores (`_`).
    DestinationId:
      type: apiKey
      name: X-Destination-Id
      in: header
      description: |
        **Legacy only.** Required when using the legacy `/{siteId}/{channelId}` routes.
        On v2 routes, the destination ID is part of the URL path and this header is not needed.

  parameters:
    siteId:
      name: siteId
      in: path
      description: Site ID to query data from
      required: true
      schema:
        type: integer
      example: 12345

    channelId:
      name: channelId
      in: path
      description: Channel ID for site-channel combination to query data from
      required: true
      schema:
        type: integer
      example: 67890

    destinationId:
      name: destinationId
      in: path
      description: Destination ID to authenticate and query data for
      required: true
      schema:
        type: integer
      example: 154110

    limitQuery:
      name: limit
      in: query
      description: Limit the total number of products returned (equivalent to LIMIT in SQL)
      required: false
      schema:
        type: integer
        minimum: 1
        default: 100
      example: 100

    offsetQuery:
      name: offset
      in: query
      description: Offset into the result set (equivalent to OFFSET in SQL)
      required: false
      schema:
        type: integer
        minimum: 0
        default: 0
      example: 0

    orderByQuery:
      name: orderBy
      in: query
      description: Sort results by the specified column (equivalent to ORDER BY in SQL)
      required: false
      schema:
        type: string
      example: "price"

    groupByQuery:
      name: groupBy
      in: query
      description: Group results by a certain column (equivalent to GROUP BY in SQL)
      required: false
      schema:
        type: string
      example: "category"

    modeQuery:
      name: mode
      in: query
      description: Top-level logical operator applied across all filter groups
      required: false
      schema:
        type: string
        enum: [AND, OR]
        default: AND
      example: "AND"

    filterQuery:
      name: filters
      in: query
      description: |
        Advanced filter groups. Encoded as PHP/Symfony bracket notation in the query string.

        Example (price > 100 AND stock > 0):
        ```
        filters[0][criteria][0][column]=price&filters[0][criteria][0][operator]=%3E&filters[0][criteria][0][value]=100
        &filters[0][criteria][1][column]=stock&filters[0][criteria][1][operator]=%3E&filters[0][criteria][1][value]=0
        &filters[0][mode]=AND
        ```
      required: false
      schema:
        type: array
        items:
          $ref: '#/components/schemas/CriteriaGroup'

  schemas:
    CriteriaGroup:
      type: object
      properties:
        criteria:
          type: array
          items:
            $ref: '#/components/schemas/CriteriaItem'
        mode:
          type: string
          enum: [OR, AND]
          default: AND
          description: Logical operator to combine criteria within this group
      example:
        criteria:
          - column: "price"
            operator: ">"
            value: 100
          - column: "stock"
            operator: ">"
            value: 0
        mode: "AND"

    CriteriaItem:
      type: object
      required:
        - column
        - operator
        - value
      properties:
        column:
          type: string
          description: Column name to filter on
          example: "price"
        operator:
          type: string
          description: Comparison operator
          enum: ["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN"]
          example: "="
        value:
          oneOf:
            - type: string
            - type: integer
            - type: number
            - type: array
              items:
                oneOf:
                  - type: string
                  - type: integer
          description: |
            Value to compare against.
            When `operator` is `IN` or `NOT IN`, this must be an array.
            For all other operators, this must be a scalar (string or number).
          example: 100

    ProductResponse:
      type: object
      description: |
        Product data object. The actual properties depend on the exported data schema.
        Below are common example properties.
      additionalProperties: true
      properties:
        id:
          type: integer
          description: Product ID
          example: 1
        name:
          type: string
          description: Product name
          example: "My Product Name"
        price:
          type: number
          description: Product price
          example: 29.99
        color:
          type: string
          description: Product color
          example: "Blue"
        category:
          type: string
          description: Product category
          example: "Electronics"

    JsonApiProductListResponse:
      type: object
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ProductResponse'
          description: |
            Array of raw product attribute objects. Note: these are not full JSON:API resource objects —
            `type` and `id` wrapper fields defined by the JSON:API specification are not included per item.
        meta:
          type: object
          required:
            - total
            - limit
            - offset
          properties:
            total:
              type: integer
              description: Total number of matching records (ignoring limit/offset)
              example: 15000
            limit:
              type: integer
              description: The limit applied to this request
              example: 100
            offset:
              type: integer
              description: The offset applied to this request
              example: 0
      example:
        data:
          - id: "abc123"
            color: "Blue"
            name: "My Product"
        meta:
          total: 15000
          limit: 100
          offset: 0

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error message (routing and auth errors)
          example: "Authentication token is required"
        message:
          type: string
          description: Error message (data-not-found errors)
          example: "No exported data"

    JsonApiErrorResponse:
      type: object
      required:
        - errors
      properties:
        errors:
          type: array
          items:
            type: object
            required:
              - status
              - title
              - detail
            properties:
              status:
                type: integer
                description: HTTP status code
                example: 401
              title:
                type: string
                description: Short human-readable error title
                example: "Unauthorized"
              detail:
                type: string
                description: Detailed error message
                example: "Authentication token is required"
      example:
        errors:
          - status: 401
            title: "Unauthorized"
            detail: "Authentication token is required"

  responses:
    UnauthorizedError:
      description: Authentication failed
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/JsonApiErrorResponse'
          example:
            errors:
              - status: 401
                title: "Unauthorized"
                detail: "Authentication token is required"
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missing_token:
              summary: Missing authentication token
              value:
                error: "Authentication token is required"
            invalid_token_format:
              summary: Invalid token format
              value:
                error: "Provided token format is invalid"
            missing_destination:
              summary: Missing destination ID
              value:
                error: "Destination ID is required"
            invalid_destination:
              summary: Invalid destination ID format
              value:
                error: "Invalid destination ID format"

    ForbiddenError:
      description: Authorization failed - token not valid for destination
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/JsonApiErrorResponse'
          example:
            errors:
              - status: 403
                title: "Forbidden"
                detail: "Token is not authorized for this destination"
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "Token is not authorized for this destination"

    NotFoundError:
      description: |
        Resource not found. Note: the response body key differs by cause —
        data-not-found returns `{"message": "..."}`, routing 404s return `{"error": "..."}`.
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/JsonApiErrorResponse'
          example:
            errors:
              - status: 404
                title: "Not Found"
                detail: "No exported data"
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            no_data:
              summary: No exported data
              value:
                message: "No exported data"
            route_not_found:
              summary: Invalid route
              value:
                error: "Requested route was not found: \"/invalid/path\""

