openapi: 3.1.0
info:
  title: Listing Assistant API
  version: 1.0.0
  description: >
    Lightweight, serverless API for generating platform-optimized listing
    titles, tags, and image analysis

    for marketplace listings (Etsy, eBay, Vinted).


    ## Authentication


    Most endpoints require an API key via the `x-api-key` header. Admin
    endpoints require `x-admin-key`.


    ## Rate Limits


    - FREE: 60 requests per 10 minutes, 20 runs per day

    - PRO: 600 requests per 10 minutes, 200 runs per day

    - BUSINESS: 6,000 requests per 10 minutes, 2000 runs per day


    ## Correlation IDs


    Send `x-correlation-id` header for request tracing, or one will be
    auto-generated.
  contact:
    name: API Support
  license:
    name: ISC
servers:
  - url: https://listinghelper.kreativschicht.de
    description: Production server
tags:
  - name: analyze
    description: Product analysis and optimization
  - name: account
    description: API key account management
  - name: billing
    description: Subscription and billing management
  - name: admin
    description: Administrative operations (requires admin key)
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key for authenticating user requests
    AdminKeyAuth:
      type: apiKey
      in: header
      name: x-admin-key
      description: Admin key for administrative operations
  schemas:
    ProductDraft:
      type: object
      properties:
        title:
          type: string
          example: Handmade Leather Wallet
        description:
          type: string
          example: Premium quality leather wallet
        category:
          type: string
          example: Accessories
        brand:
          type: string
          example: Artisan Goods
        condition:
          type: string
          example: New
        price:
          type: number
          format: float
          example: 49.99
        color:
          type: string
          example: Brown
        size:
          type: string
          example: Standard
        material:
          type: string
          example: Leather
      additionalProperties: true
    Platform:
      type: string
      enum:
        - etsy
        - ebay
        - vinted
    PlanTier:
      type: string
      enum:
        - FREE
        - PRO
        - BUSINESS
    FeatureFlags:
      type: object
      required:
        - enableTitles
        - enableTags
        - enableImageChecks
      properties:
        enableTitles:
          type: boolean
        enableTags:
          type: boolean
        enableImageChecks:
          type: boolean
    AnalyzeRequest:
      type: object
      required:
        - productDraft
        - platforms
        - flags
      properties:
        productDraft:
          $ref: '#/components/schemas/ProductDraft'
        platforms:
          type: array
          items:
            $ref: '#/components/schemas/Platform'
          minItems: 1
        flags:
          $ref: '#/components/schemas/FeatureFlags'
        images:
          type: array
          items:
            type: string
            format: uri
          description: Optional array of image URLs to analyze
    TitleSuggestion:
      type: object
      required:
        - platform
        - title
        - score
        - reasons
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        title:
          type: string
        score:
          type: number
          format: int32
          description: Score from 0-120
        reasons:
          type: array
          items:
            type: string
        issues:
          type: array
          items:
            type: string
    TagGrouped:
      type: object
      properties:
        material:
          type: array
          items:
            type: string
        style:
          type: array
          items:
            type: string
        usecase:
          type: array
          items:
            type: string
        audience:
          type: array
          items:
            type: string
        occasion:
          type: array
          items:
            type: string
        generic:
          type: array
          items:
            type: string
    TagSuggestion:
      type: object
      required:
        - platform
        - tags
        - normalized
        - tagsCommaSeparated
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        tags:
          type: array
          items:
            type: string
        normalized:
          type: array
          items:
            type: string
        tagsCommaSeparated:
          type: string
          description: Tags as comma-separated string for easy copying
        grouped:
          $ref: '#/components/schemas/TagGrouped'
    ImageMetrics:
      type: object
      required:
        - width
        - height
        - aspectRatio
        - fileSizeBytes
        - brightness
        - blurScore
        - hasWatermarkLikely
      properties:
        width:
          type: integer
        height:
          type: integer
        aspectRatio:
          type: number
          format: float
        fileSizeBytes:
          type: integer
        brightness:
          type: number
          format: float
        blurScore:
          type: number
          format: float
        hasWatermarkLikely:
          type: boolean
    ImageIssue:
      type: object
      required:
        - severity
        - message
      properties:
        severity:
          type: string
          enum:
            - error
            - warning
            - info
        message:
          type: string
        suggestedFix:
          type: string
    ImageReport:
      type: object
      required:
        - url
        - hash
        - metrics
        - issues
      properties:
        url:
          type: string
          format: uri
        hash:
          type: string
          description: SHA-256 hash of image
        metrics:
          $ref: '#/components/schemas/ImageMetrics'
        issues:
          type: array
          items:
            $ref: '#/components/schemas/ImageIssue'
    CacheDiagnostics:
      type: object
      properties:
        hit:
          type: boolean
        keyPrefix:
          type: string
        ageSeconds:
          type: integer
    Diagnostics:
      type: object
      required:
        - timings
        - rulesVersion
      properties:
        timings:
          type: object
          required:
            - total
          properties:
            total:
              type: integer
              description: Total time in milliseconds
            titles:
              type: integer
            tags:
              type: integer
            images:
              type: integer
        rulesVersion:
          type: string
        cache:
          $ref: '#/components/schemas/CacheDiagnostics'
        llmWarning:
          type: string
          description: Warning message when LLM is disabled due to budget
    UsageInfo:
      type: object
      properties:
        runsToday:
          type: integer
        runsRemaining:
          type: integer
        plan:
          $ref: '#/components/schemas/PlanTier'
    AnalyzeResponse:
      type: object
      required:
        - titles
        - tags
        - imageReport
        - diagnostics
      properties:
        titles:
          type: array
          items:
            $ref: '#/components/schemas/TitleSuggestion'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/TagSuggestion'
        imageReport:
          type: array
          items:
            $ref: '#/components/schemas/ImageReport'
        diagnostics:
          $ref: '#/components/schemas/Diagnostics'
        usage:
          $ref: '#/components/schemas/UsageInfo'
    ApiKeyInfo:
      type: object
      required:
        - id
        - keyPrefix
        - name
        - plan
        - createdAt
        - isActive
      properties:
        id:
          type: string
          example: key_1234567890_abcdef1234567890
        keyPrefix:
          type: string
          example: ak_12345678...
        name:
          type: string
          example: Production Key
        plan:
          $ref: '#/components/schemas/PlanTier'
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
        isActive:
          type: boolean
    UsageRecord:
      type: object
      properties:
        date:
          type: string
          format: date
        runs:
          type: integer
        tokensUsed:
          type: integer
    UsageResponse:
      type: object
      required:
        - success
        - apiKey
        - usage
      properties:
        success:
          type: boolean
        apiKey:
          $ref: '#/components/schemas/ApiKeyInfo'
        usage:
          type: array
          items:
            $ref: '#/components/schemas/UsageRecord'
        summary:
          type: object
          properties:
            totalRuns:
              type: integer
            totalTokens:
              type: integer
            daysQueried:
              type: integer
    RotateKeyResponse:
      type: object
      required:
        - success
        - apiKey
        - keyId
        - keyPrefix
        - message
      properties:
        success:
          type: boolean
        apiKey:
          type: string
          description: New API key (shown only once)
          example: ak_newkey567890abcdef...
        keyId:
          type: string
        keyPrefix:
          type: string
        message:
          type: string
    CreateKeyRequest:
      type: object
      required:
        - name
        - plan
      properties:
        name:
          type: string
          example: Production Key
        plan:
          $ref: '#/components/schemas/PlanTier'
    CreateKeyResponse:
      type: object
      required:
        - success
        - apiKey
        - keyId
        - keyPrefix
        - plan
        - name
        - message
      properties:
        success:
          type: boolean
        apiKey:
          type: string
          description: API key (shown only once)
        keyId:
          type: string
        keyPrefix:
          type: string
        plan:
          $ref: '#/components/schemas/PlanTier'
        name:
          type: string
        message:
          type: string
    ListKeysResponse:
      type: object
      required:
        - success
        - keys
        - count
      properties:
        success:
          type: boolean
        keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyInfo'
        count:
          type: integer
    CheckoutRequest:
      type: object
      required:
        - plan
      properties:
        plan:
          type: string
          enum:
            - PRO
            - BUSINESS
    CheckoutResponse:
      type: object
      required:
        - success
        - checkoutUrl
        - sessionId
      properties:
        success:
          type: boolean
        checkoutUrl:
          type: string
          format: uri
        sessionId:
          type: string
    PortalResponse:
      type: object
      required:
        - success
        - portalUrl
      properties:
        success:
          type: boolean
        portalUrl:
          type: string
          format: uri
    ErrorResponse:
      type: object
      required:
        - error
        - code
        - message
      properties:
        error:
          type: string
          example: Bad Request
        code:
          type: string
          example: VALIDATION_ERROR
        message:
          type: string
        details:
          type: object
          additionalProperties: true
        retryAfterSeconds:
          type: integer
          description: Present for rate limit errors (429)
        requestCount:
          type: integer
          description: Present for rate limit errors (429)
        limit:
          type: integer
          description: Present for rate limit errors (429)
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Unauthorized
            code: INVALID_API_KEY
            message: Invalid or expired API key
    PaymentRequired:
      description: Plan limits exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Payment Required
            code: PLAN_LIMIT_EXCEEDED
            message: Your plan allows maximum 1 platform per request. You requested 2.
    TooManyRequests:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Too Many Requests
            code: RATE_LIMIT_EXCEEDED
            message: >-
              Rate limit exceeded. You have made 61 requests in the current
              10-minute window.
            retryAfterSeconds: 420
            requestCount: 61
            limit: 60
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Internal Server Error
            code: INTERNAL_ERROR
            message: An unexpected error occurred
  parameters:
    CorrelationId:
      name: x-correlation-id
      in: header
      required: false
      schema:
        type: string
        format: uuid
      description: Optional correlation ID for request tracing
paths:
  /api/analyze:
    post:
      tags:
        - analyze
      summary: Analyze product draft
      description: >
        Generate platform-optimized titles, tags, and image analysis for a
        product draft.


        This endpoint is the core of the Listing Assistant. It takes product
        information,

        platform targets, and feature flags, then returns optimized suggestions.
      operationId: analyzeProduct
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeRequest'
            example:
              productDraft:
                title: Handmade Leather Wallet
                description: Premium quality leather wallet
                category: Accessories
                brand: Artisan Goods
                condition: New
                price: 49.99
              platforms:
                - etsy
                - ebay
              flags:
                enableTitles: true
                enableTags: true
                enableImageChecks: false
              images: []
      responses:
        '200':
          description: Successful analysis
          headers:
            X-Correlation-Id:
              schema:
                type: string
              description: Correlation ID for this request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyzeResponse'
        '400':
          description: Bad request - validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/me:
    get:
      tags:
        - account
      summary: Get API key information
      description: >-
        Returns information about the authenticated API key (plan, creation
        date, usage stats)
      operationId: getMe
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: API key information
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - apiKey
                properties:
                  success:
                    type: boolean
                  apiKey:
                    $ref: '#/components/schemas/ApiKeyInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/me/usage:
    get:
      tags:
        - account
      summary: Get usage statistics
      description: >-
        Returns usage statistics for the authenticated API key over a specified
        time period
      operationId: getUsage
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 30
          description: Number of days to retrieve usage for (max 90)
      responses:
        '200':
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/me/rotate-key:
    post:
      tags:
        - account
      summary: Rotate API key
      description: |
        Generates a new API key and invalidates the current one.
        The new key is returned in the response and must be saved immediately.
      operationId: rotateKey
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Key rotated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/billing/checkout:
    post:
      tags:
        - billing
      summary: Create checkout session
      description: >
        Creates a Stripe Checkout session to upgrade the API key to PRO or
        BUSINESS plan.

        Returns a URL to redirect the user to complete payment.
      operationId: createCheckout
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
            example:
              plan: PRO
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
        '400':
          description: Invalid plan or already subscribed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/billing/portal:
    post:
      tags:
        - billing
      summary: Create customer portal session
      description: |
        Creates a Stripe Customer Portal session for managing subscriptions.
        Returns a URL to redirect the user to the portal.
      operationId: createPortal
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Portal session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalResponse'
        '400':
          description: No active subscription found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/billing/webhook:
    post:
      tags:
        - billing
      summary: Stripe webhook endpoint
      description: >
        **Server-to-server only** - This endpoint is called by Stripe, not by
        clients.


        Handles Stripe webhook events for subscription lifecycle management.

        Requires valid Stripe signature in the `stripe-signature` header.
      operationId: handleWebhook
      parameters:
        - name: stripe-signature
          in: header
          required: true
          schema:
            type: string
          description: Stripe webhook signature for verification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Stripe event payload
      responses:
        '200':
          description: Webhook processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
        '400':
          description: Invalid signature or payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/admin/keys:
    post:
      tags:
        - admin
      summary: Create new API key
      description: |
        Creates a new API key with specified plan.
        **Admin only** - requires x-admin-key header.
      operationId: createApiKey
      security:
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateKeyRequest'
      responses:
        '200':
          description: API key created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateKeyResponse'
        '401':
          description: Invalid or missing admin key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
    get:
      tags:
        - admin
      summary: List all API keys
      description: |
        Lists all API keys (without secrets).
        **Admin only** - requires x-admin-key header.
      operationId: listApiKeys
      security:
        - AdminKeyAuth: []
      responses:
        '200':
          description: List of API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListKeysResponse'
        '401':
          description: Invalid or missing admin key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/admin/keys/rotate:
    post:
      tags:
        - admin
      summary: Rotate API key
      description: |
        Rotates an existing API key (invalidates old, generates new).
        **Admin only** - requires x-admin-key header.
      operationId: adminRotateKey
      security:
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - keyId
              properties:
                keyId:
                  type: string
                  example: key_1234567890_abcdef1234567890
      responses:
        '200':
          description: Key rotated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateKeyResponse'
        '401':
          description: Invalid or missing admin key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/openapi:
    get:
      tags:
        - documentation
      summary: Get OpenAPI specification
      description: Returns the OpenAPI 3.1 specification in YAML or JSON format
      operationId: getOpenApiSpec
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - yaml
              - json
            default: yaml
          description: Response format (yaml or json)
      responses:
        '200':
          description: OpenAPI specification
          content:
            application/x-yaml:
              schema:
                type: string
            application/json:
              schema:
                type: object
        '500':
          $ref: '#/components/responses/InternalServerError'
