openapi: 3.0.3
info:
  title: Knitido B2B Partner API - Product & Stock
  description: |
    Product information and stock data API for B2B partners.
    
    ## Authentication
    All requests require the `X-API-Key` header.
    API keys are issued by the administrator.
    
    ## Rate Limiting
    - Standard: 100 requests/minute
    - Premium: 1000 requests/minute
    
    Rate limit information is returned in response headers.
    
    ## Data Freshness
    Product and stock data is synchronized from the ERP system hourly.
    There may be a delay of up to 60 minutes.

    ## Multilingual Support
    This API supports 14 languages for product descriptions:
    - **Full coverage (all products)**: German (de), English (en)
    - **Partial coverage**: Japanese (ja), French (fr), Spanish (es), Italian (it), Dutch (nl), Polish (pl), Latvian (lv), Estonian (et), Lithuanian (lt), Finnish (fi), Swedish (sv), Norwegian (no)

    Default language is German (de). Language coverage for non-German/English languages will be expanded incrementally.
  version: 1.0.0
  contact:
    name: API Support
    email: support@knitido.de

servers:
  - url: https://api.knitido.de/v1
    description: Production (prd)
  - url: https://api-stg.knitido.de/v1
    description: Staging (stg)

security:
  - ApiKeyAuth: []

tags:
  - name: Products
    description: Product information retrieval
  - name: Stock
    description: Stock information retrieval

paths:
  /products:
    get:
      tags:
        - Products
      summary: Get products (list)
      description: |
        Retrieve a list of products with pagination support.
        Use the `updated_since` parameter for delta synchronization.
      operationId: getProducts
      parameters:
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/OffsetParam'
        - name: updated_since
          in: query
          description: Retrieve only products synchronized after the specified date/time (ISO 8601 format)
          schema:
            type: string
            format: date-time
            example: "2025-09-01T00:00:00Z"
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Product'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                data:
                  - item_id: 177
                    variation_id: 1466
                    sku: "61048164346"
                    name: "Knitido Traditionals Tabi Sneaker Zwei-Zehensocken"
                    price: "6.77"
                    categories:
                      - "Zehensocken"
                      - "B2B-Produkt"
                      - "Tabi"
                      - "Sneaker"
                    category_ids: [21, 29, 35, 51]
                    attributes:
                      size: "43-46"
                      color: "anthrazit (816)"
                    last_synced_at: "2025-09-02T07:28:09Z"
                  - item_id: 219
                    variation_id: 1908
                    sku: "53010073538"
                    name: "Dr. Foot Plattfuß Zehensocken"
                    price: "13.60"
                    categories:
                      - "Zehensocken"
                      - "B2B-Produkt"
                      - "Hallux Valgus"
                      - "Midi"
                    category_ids: [21, 29, 46, 52]
                    attributes:
                      size: "35-38"
                      color: "beige (7)"
                    last_synced_at: "2025-09-02T07:28:09Z"
                meta:
                  total: 150
                  limit: 20
                  offset: 0
                  data_freshness:
                    sync_frequency: "hourly"
                    max_delay_minutes: 60
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

  /products/{sku}:
    get:
      tags:
        - Products
      summary: Get product (single SKU)
      description: Retrieve product information for the specified SKU.
      operationId: getProductBySku
      parameters:
        - $ref: '#/components/parameters/SkuPath'
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Product'
              example:
                success: true
                data:
                  item_id: 148
                  variation_id: 1132
                  sku: "110061-55-3942"
                  name: "Knitido Plus \"Yama\" Posture-Zehensocken mit ABS"
                  price: "11.16"
                  categories:
                    - "Zehensocken"
                    - "B2B-Produkt"
                    - "Knitido+"
                    - "non-slip"
                  category_ids: [21, 29, 39, 55]
                  attributes:
                    size: "39-42"
                    color: "indigo (55)"
                  last_synced_at: "2025-09-02T07:28:09Z"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

  /products/{sku}/descriptions:
    get:
      tags:
        - Products
      summary: Get product descriptions
      description: |
        Retrieve product descriptions for the specified SKU.

        - `lang` omitted: Returns all languages as an array
        - `lang=de`: Returns German only (array with 1 element)
        - `lang=de,en`: Returns German and English (array)
        - `lang=xyz` (non-existent): Returns empty array

        Note: Always returns an array for consistency.
      operationId: getProductDescriptions
      parameters:
        - $ref: '#/components/parameters/SkuPath'
        - name: lang
          in: query
          description: |
            Specify language codes separated by commas.
            If omitted, all languages are returned.

            Supported languages (14 total):
            - Full coverage: de (default), en
            - Partial coverage: ja, fr, es, it, nl, pl, lv, et, lt, fi, sv, no
          schema:
            type: string
            example: "de,en"
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ProductDescription'
                        description: Array of product descriptions (may be empty if language not found)
              examples:
                single_language:
                  summary: Single language (?lang=de)
                  value:
                    success: true
                    data:
                      - item_id: 177
                        lang: "de"
                        name1: "Knitido Traditionals Tabi Sneaker Zwei-Zehensocken"
                        name2: null
                        name3: "Tabi Sneaker"
                        short_description: "<p>Klassische Tabi-Socken im Sneaker-Schnitt...</p>"
                        description: "<p>Die Knitido Traditionals Tabi Sneaker sind...</p>"
                        technical_data: "<table><tr><td>Material</td><td>85% Baumwolle...</td></tr></table>"
                        last_synced_at: "2025-09-02T07:28:09Z"
                multiple_languages:
                  summary: Multiple languages (?lang=de,en)
                  value:
                    success: true
                    data:
                      - item_id: 177
                        lang: "de"
                        name1: "Knitido Traditionals Tabi Sneaker Zwei-Zehensocken"
                        name2: null
                        name3: "Tabi Sneaker"
                        short_description: "<p>Klassische Tabi-Socken im Sneaker-Schnitt...</p>"
                        description: "<p>Die Knitido Traditionals Tabi Sneaker sind...</p>"
                        technical_data: "<table><tr><td>Material</td><td>85% Baumwolle...</td></tr></table>"
                        last_synced_at: "2025-09-02T07:28:09Z"
                      - item_id: 177
                        lang: "en"
                        name1: "Knitido Traditionals Tabi Sneaker Split Toe Socks"
                        name2: null
                        name3: "Tabi Sneaker"
                        short_description: "<p>Classic Tabi socks in sneaker cut...</p>"
                        description: "<p>The Knitido Traditionals Tabi Sneaker are...</p>"
                        technical_data: "<table><tr><td>Material</td><td>85% Cotton...</td></tr></table>"
                        last_synced_at: "2025-09-02T07:28:09Z"
                non_existent_language:
                  summary: Non-existent language (?lang=xyz)
                  value:
                    success: true
                    data: []
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

  /descriptions:
    get:
      tags:
        - Products
      summary: Get product descriptions (bulk)
      description: |
        Retrieve product descriptions for all products with pagination support.
        Use this endpoint for efficient bulk data synchronization.

        - `lang` omitted: Returns all languages
        - `lang=de`: Returns German only (single language for efficiency)
        - Use `updated_since` for delta synchronization

        Note: Unlike `/products/{sku}/descriptions`, this endpoint accepts only a single language code (not comma-separated).
      operationId: getDescriptions
      parameters:
        - name: lang
          in: query
          description: |
            Specify a single language code to filter descriptions.
            If omitted, all languages are returned.

            Supported languages (14 total):
            - Full coverage: de (default), en
            - Partial coverage: ja, fr, es, it, nl, pl, lv, et, lt, fi, sv, no

            Note: German and English cover all products. Other languages may have partial coverage.
          schema:
            type: string
            example: "de"
        - name: limit
          in: query
          description: "Number of records to retrieve (default: 100, max: 500)"
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
            example: 100
        - $ref: '#/components/parameters/OffsetParam'
        - name: updated_since
          in: query
          description: Retrieve only descriptions synchronized after the specified date/time (ISO 8601 format)
          schema:
            type: string
            format: date-time
            example: "2025-09-01T00:00:00Z"
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ProductDescription'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              examples:
                single_language:
                  summary: Single language (?lang=de)
                  value:
                    success: true
                    data:
                      - item_id: 177
                        lang: "de"
                        name1: "Knitido Traditionals Tabi Sneaker"
                        name2: null
                        name3: "Tabi Sneaker"
                        short_description: "<p>Kurze Beschreibung...</p>"
                        description: "<p>Vollständige Beschreibung...</p>"
                        technical_data: "<table>...</table>"
                        last_synced_at: "2025-01-15T10:45:00Z"
                      - item_id: 219
                        lang: "de"
                        name1: "Dr. Foot Plattfuß Zehensocken"
                        name2: null
                        name3: "Plattfuß"
                        short_description: "<p>Kurze Beschreibung...</p>"
                        description: "<p>Vollständige Beschreibung...</p>"
                        technical_data: "<table>...</table>"
                        last_synced_at: "2025-01-15T10:45:00Z"
                    meta:
                      total: 450
                      limit: 100
                      offset: 0
                      data_freshness:
                        sync_frequency: "hourly"
                        max_delay_minutes: 60
                delta_sync:
                  summary: Delta sync (?lang=de&updated_since=2025-01-15T00:00:00Z)
                  value:
                    success: true
                    data:
                      - item_id: 177
                        lang: "de"
                        name1: "Knitido Traditionals Tabi Sneaker (Updated)"
                        name2: null
                        name3: "Tabi Sneaker"
                        short_description: "<p>Aktualisierte kurze Beschreibung...</p>"
                        description: "<p>Aktualisierte vollständige Beschreibung...</p>"
                        technical_data: "<table>...</table>"
                        last_synced_at: "2025-01-15T14:30:00Z"
                    meta:
                      total: 5
                      limit: 100
                      offset: 0
                      data_freshness:
                        sync_frequency: "hourly"
                        max_delay_minutes: 60
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

  /stock:
    get:
      tags:
        - Stock
      summary: Get stock (list / multiple SKUs)
      description: |
        Retrieve stock information.
        
        - `skus` omitted: Retrieve all stock with pagination
        - `skus` specified: Retrieve stock for specified SKUs only (pagination disabled)
      operationId: getStock
      parameters:
        - name: skus
          in: query
          description: Specify SKUs separated by commas. If omitted, all records are retrieved.
          schema:
            type: string
            example: "61048164346,53010073538,110061-55-3942"
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/OffsetParam'
        - name: in_stock
          in: query
          description: Retrieve only items in stock (stock > 0)
          schema:
            type: boolean
            example: true
        - name: updated_since
          in: query
          description: Retrieve only stock synchronized after the specified date/time (ISO 8601 format)
          schema:
            type: string
            format: date-time
            example: "2025-09-01T00:00:00Z"
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Stock'
                      meta:
                        oneOf:
                          - $ref: '#/components/schemas/PaginationMeta'
                          - $ref: '#/components/schemas/SkuRequestMeta'
              examples:
                all_stock:
                  summary: All stock
                  value:
                    success: true
                    data:
                      - item_id: 177
                        variation_id: 1466
                        sku: "61048164346"
                        stock: 150
                        last_synced_at: "2025-09-02T07:30:00Z"
                      - item_id: 219
                        variation_id: 1908
                        sku: "53010073538"
                        stock: 42
                        last_synced_at: "2025-09-02T07:30:00Z"
                    meta:
                      total: 500
                      limit: 20
                      offset: 0
                specific_skus:
                  summary: Specific SKUs
                  value:
                    success: true
                    data:
                      - item_id: 177
                        variation_id: 1466
                        sku: "61048164346"
                        stock: 150
                        last_synced_at: "2025-09-02T07:30:00Z"
                      - item_id: 219
                        variation_id: 1908
                        sku: "53010073538"
                        stock: 42
                        last_synced_at: "2025-09-02T07:30:00Z"
                      - item_id: 148
                        variation_id: 1132
                        sku: "110061-55-3942"
                        stock: 0
                        last_synced_at: "2025-09-02T07:30:00Z"
                    meta:
                      requested: 3
                      found: 3
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

  /stock/{sku}:
    get:
      tags:
        - Stock
      summary: Get stock (single SKU)
      description: Retrieve stock information for the specified SKU.
      operationId: getStockBySku
      parameters:
        - $ref: '#/components/parameters/SkuPath'
      responses:
        '200':
          description: Success
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Stock'
              example:
                success: true
                data:
                  item_id: 177
                  variation_id: 1466
                  sku: "61048164346"
                  stock: 150
                  last_synced_at: "2025-09-02T07:30:00Z"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API Key authentication. Specify the key issued by the administrator.
        
        Format: `b2b_prd_xxxxxxxxxxxxxxxx` (production) or `b2b_stg_xxxxxxxxxxxxxxxx` (staging)

  parameters:
    SkuPath:
      name: sku
      in: path
      required: true
      description: Product SKU
      schema:
        type: string
        example: "61048164346"

    LimitParam:
      name: limit
      in: query
      description: "Number of records to retrieve (default: 20, max: 100)"
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
        example: 20

    OffsetParam:
      name: offset
      in: query
      description: Offset for pagination
      schema:
        type: integer
        minimum: 0
        default: 0
        example: 0

  headers:
    X-RateLimit-Limit:
      description: Rate limit ceiling
      schema:
        type: integer
        example: 100

    X-RateLimit-Remaining:
      description: Remaining requests in current window
      schema:
        type: integer
        example: 95

    X-RateLimit-Reset:
      description: Rate limit reset time (Unix timestamp)
      schema:
        type: integer
        example: 1704067200

  schemas:
    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
      required:
        - success

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code
            message:
              type: string
              description: Error message
          required:
            - code
            - message
      required:
        - success
        - error

    Product:
      type: object
      properties:
        item_id:
          type: integer
          description: plentyONE Item ID
          example: 177
        variation_id:
          type: integer
          description: plentyONE Variation ID
          example: 1466
        sku:
          type: string
          description: Product SKU (variation number)
          example: "61048164346"
        name:
          type: string
          description: Product name (default language)
          example: "Knitido Traditionals Tabi Sneaker Zwei-Zehensocken"
        price:
          type: string
          description: B2B Net price (excl. 19% VAT, calculated from Gross price / 1.19)
          example: "6.77"
        categories:
          type: array
          items:
            type: string
          description: Array of category names
          example: ["Zehensocken", "B2B-Produkt", "Tabi", "Sneaker"]
        category_ids:
          type: array
          items:
            type: integer
          description: Array of category IDs
          example: [21, 29, 35, 51]
        attributes:
          type: object
          additionalProperties:
            type: string
          description: Variation attributes (size, color, etc.)
          example:
            size: "43-46"
            color: "anthrazit (816)"
        last_synced_at:
          type: string
          format: date-time
          description: Last synchronization date/time (ISO 8601)
          example: "2025-09-02T07:28:09Z"
      required:
        - item_id
        - variation_id
        - sku
        - name
        - price
        - categories
        - category_ids
        - attributes
        - last_synced_at

    ProductDescription:
      type: object
      properties:
        item_id:
          type: integer
          description: plentyONE Item ID
          example: 177
        lang:
          type: string
          description: |
            Language code (14 languages supported).
            - Full coverage: de (German, default), en (English)
            - Partial coverage: ja, fr, es, it, nl, pl, lv, et, lt, fi, sv, no
          example: "de"
        name1:
          type: string
          description: Product name 1 (main)
          example: "Knitido Traditionals Tabi Sneaker Zwei-Zehensocken"
        name2:
          type: string
          nullable: true
          description: Product name 2 (sub)
          example: null
        name3:
          type: string
          nullable: true
          description: Product name 3 (short)
          example: "Tabi Sneaker"
        short_description:
          type: string
          nullable: true
          description: Short description (HTML)
          example: "<p>Klassische Tabi-Socken im Sneaker-Schnitt...</p>"
        description:
          type: string
          nullable: true
          description: Full description (HTML)
          example: "<p>Die Knitido Traditionals Tabi Sneaker sind...</p>"
        technical_data:
          type: string
          nullable: true
          description: Technical specifications (HTML)
          example: "<table><tr><td>Material</td><td>85% Baumwolle...</td></tr></table>"
        last_synced_at:
          type: string
          format: date-time
          description: Last synchronization date/time (ISO 8601)
          example: "2025-09-02T07:28:09Z"
      required:
        - item_id
        - lang
        - name1
        - last_synced_at

    Stock:
      type: object
      properties:
        item_id:
          type: integer
          description: plentyONE Item ID
          example: 177
        variation_id:
          type: integer
          description: plentyONE Variation ID
          example: 1466
        sku:
          type: string
          description: Product SKU
          example: "61048164346"
        stock:
          type: integer
          description: Stock quantity
          example: 150
        last_synced_at:
          type: string
          format: date-time
          description: Last synchronization date/time (ISO 8601)
          example: "2025-09-02T07:30:00Z"
      required:
        - item_id
        - variation_id
        - sku
        - stock
        - last_synced_at

    PaginationMeta:
      type: object
      properties:
        total:
          type: integer
          description: Total number of records
          example: 150
        limit:
          type: integer
          description: Number of records per page
          example: 20
        offset:
          type: integer
          description: Current offset
          example: 0
        data_freshness:
          type: object
          properties:
            sync_frequency:
              type: string
              description: Synchronization frequency
              example: "hourly"
            max_delay_minutes:
              type: integer
              description: Maximum delay in minutes
              example: 60

    SkuRequestMeta:
      type: object
      properties:
        requested:
          type: integer
          description: Number of SKUs requested
          example: 3
        found:
          type: integer
          description: Number of SKUs found
          example: 3

  responses:
    ValidationError:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: "VALIDATION_ERROR"
              message: "Invalid parameter: limit must be between 1 and 100"

    UnauthorizedError:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: "UNAUTHORIZED"
              message: "Invalid or missing API key"

    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: "NOT_FOUND"
              message: "Product not found: INVALID-SKU"

    RateLimitError:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          schema:
            type: integer
            example: 0
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          description: Seconds until retry is allowed
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: "RATE_LIMIT_EXCEEDED"
              message: "Rate limit exceeded. Please try again later."

    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: "INTERNAL_ERROR"
              message: "Internal server error"
