openapi: 3.0.3
info:
  title: Descrybe Public API
  version: '1.0'
  description: |
    Public catalog, feed, and product-processing API.

    ## Base URL

    The public API is hosted by Descrybe (customers do not self-host this surface).

    - Production: https://descrybe.io/api/v1

    OpenAPI document: GET https://descrybe.io/api/v1/openapi.yaml

    ## Authentication

    All /api/v1 operations require a company API key except:

    Document-level security is BearerAuth OR ApiKeyAuth (same key value).
    Do not send dashboard session cookies or CSRF tokens to /api/v1.

    ### Security schemes (components.securitySchemes)

    - BearerAuth - HTTP bearer. Authorization: Bearer dk_your_key (preferred)
    - ApiKeyAuth - header X-API-Key: dk_your_key

    When both headers are set, Bearer wins. Full key values are never embedded in
    this YAML. RapiDoc Try-it: paste a key, or when logged into the docs page use
    "Use my API key" (coordinates with the in-app authorize helper).

    ### Create a key in the app

    1. Sign in at https://descrybe.io
    2. Open Settings -> API keys (/settings?tab=api-keys)
    3. Company admins create a key via dashboard POST /api/api-keys
        (session cookie + CSRF; not this public OpenAPI surface). The secret is
        shown once and starts with dk_.
    4. Store it securely. Later list/revoke shows only key_prefix (first 10
        characters). Revoked keys fail auth immediately.

    API keys are created in Settings -> API keys
    (or Use my API key on /docs). Pre-cutover secrets return the same HTTP 401
    Unauthorized as unknown keys — there is no separate “legacy key” error.

    ### 401 Unauthorized

    Missing, empty, unknown, revoked, or non-migrated (pre-cutover) keys return
    HTTP 401 from RequireAPIKey with the legacy coded envelope:

        { "error": { "code": "unauthorized", "message": "Unauthorized" } }

    See components.responses.Unauthorized (schema LegacyAPIError). Reissue via
    Settings -> API keys (/settings?tab=api-keys).

    ### 403 Forbidden

    Bad API keys on /api/v1 never return 403 (always 401). Tenant scope comes
    from the key; cross-company resources typically 404. HTTP 403 appears on
    dashboard /api/* session routes (admin required, CSRF mismatch) under
    Team/Admin tags (SessionCookie + CSRFHeader) - not this public key surface.

    ### Rate limits

    Heavy mutations are limited to 30 requests per minute per company
    (in-process per API replica; not shared across replicas). Counts HTTP
    requests, not products inside a bulk body. Limited POST paths:

    - /products/process, /process, /process/{id}/retry
    - /feeds/{id}/sync, /feeds/{id}/extract-schema, /feeds/{id}/sync-process-sample
    - /export-feeds/{id}/generate, /export-feeds/{id}/export-products, /export-feeds/{id}/rotate-token (admin)

    Over limit: HTTP 429, header Retry-After: 60, body
    { "error": "rate limit exceeded" } (see TooManyRequests). Ordinary GETs and
    other mutations are outside this HTTP budget (process starts may still return
    402 for plan/credits).

    ## Quick curl

        curl -s -H "Authorization: Bearer dk_your_key" \
          "https://descrybe.io/api/v1/products?page=1&limit=1"

    Local Go API (default listen from README):

        curl -s -H "Authorization: Bearer dk_your_key" \
          "http://localhost:28471/api/v1/products?page=1&limit=1"

    Health: GET /api/v1/health (also /healthz and /readyz on the API host).

    ## Processing contracts (dual-mode)

    Two separate surfaces — do not mix bodies or response envelopes:

    1. **Legacy public process (source of truth for integrations)**
       - POST /products/process with body items[].ean
       - GET /products/process/{id}
       - Envelope: HTTP 200 { data: { process_id, … } } (and completed items[])
       - Matches legacy Descrybe /api/v1/products/process
       - Handler also accepts raw_product_ids as an alternate body on this path
       - Plan gates (credits / product limit / AI / EPREL / feature flags) run before
         EnsureRaw catalog writes on the items[].ean path; blocked starts return HTTP 402

    2. **Internal / dashboard-style jobs**
       - POST /process with raw_product_ids (same body as POST /api/processing/jobs)
       - GET /process, GET /process/{id}, cancel/terminate/retry
       - Flat JSON (no data wrapper); 202 Accepted on start/retry
       - Plan gates return HTTP 402 with PlanGateError (error + code + upgrade_url)

    ### Dual IDs (do not confuse)

    - GET /products data[].id = processed_products.id (enriched row)
    - GET /products data[].raw_product_id = raw_products.id (use this for raw_product_ids)
    - POST ... raw_product_ids[] must be raw_products.id — never PresentProduct.id
    - GET /products/process/{id} COMPLETED items[].id = processed_products.id (legacy);
      additive processed_product_id (same as id) and raw_product_id (raw_products.id)

    Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
    from this public API-key surface. Other legacy path aliases
    (/categories/create, /attributes/create, /campaigns) appear next to canonical paths.
servers:
- url: https://descrybe.io/api/v1
  description: Production (Descrybe-hosted)
- url: http://localhost:28472/api/v1
  description: Local web (Vite proxy to Go API)
- url: http://localhost:28471/api/v1
  description: Local API (default Go listen address)
security:
- BearerAuth: []
- ApiKeyAuth: []
tags:
- name: Health
- name: Products
- name: Categories
- name: Attributes
- name: Feeds
- name: Export feeds
- name: Campaigns
  description: Seasonal content calendar (legacy /campaigns aliases)
- name: Processing
- name: Team
  description: Dashboard session routes under /api (not API-key)
- name: Admin
  description: Platform-admin dashboard routes under /api (not API-key)
paths:
  /health:
    get:
      tags:
      - Health
      security: []
      summary: Liveness (same payload as /healthz)
      description: |
        Returns process liveness plus cutover flags (maintenance, read_only, hypercare).
        Host-level probes /healthz and /readyz share the same flag fields;
        /readyz additionally reports database checks.
      responses:
        '200':
          description: Liveness OK. No API key required. Same flag fields as host /healthz (status, service,
            maintenance, read_only, hypercare).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                status: ok
                service: api
                maintenance: false
                read_only: false
                hypercare: false
        '500':
          description: Unexpected failure building the health payload (rare process fault).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: health check failed
  /openapi.yaml:
    get:
      tags:
      - Health
      security: []
      summary: This OpenAPI document
      responses:
        '200':
          description: This OpenAPI document. Served with Cache-Control and ETag; gzip when Accept-Encoding
            allows.
          content:
            application/yaml:
              schema:
                type: string
              example: |
                openapi: 3.0.3
                info:
                  title: Descrybe Public API
                  version: "1.0"
        '304':
          description: Not Modified — request If-None-Match matched the document ETag. Empty body.
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
  /products:
    get:
      tags:
      - Products
      summary: List products
      description: |
        Legacy-compatible public list. Envelope is { data, meta } (not flat products/offset).
        Query params match legacy: page, limit (default 25), status, search, sortBy, sortOrder, feedId.
        Each row matches PresentProduct (id = processed_products.id, raw_product_id = raw_products.id,
        product_id, name, category, status, feed_id, quality_score, quality_grade, created_at, updated_at).
        For POST /products/process or POST /process dual-mode bodies, pass raw_product_id — not id.
      parameters:
      - $ref: "#/components/parameters/Page"
      - $ref: "#/components/parameters/LegacyLimit"
      - in: query
        name: status
        schema:
          type: string
          enum:
          - all
          - needs_review
          - processed
          - completed
          - error
          - processing
          - unprocessed
          default: all
        description: |
          Filter by product status. all (default) returns every status. needs_review also matches
          legacy status processed (pre-P0-8 AI review queue). Use completed after Accept enrichment.
      - in: query
        name: search
        schema:
          type: string
          maxLength: 200
        description: |
          Free-text search over product name / product_id. Alias q is also accepted.
          Omit or empty to skip text filtering. Max ~200 characters.
      - in: query
        name: sortBy
        schema:
          type: string
          enum:
          - updatedAt
          - createdAt
          - name
          default: updatedAt
        description: |
          Sort column for the product list. One of updatedAt (default), createdAt, or name.
      - in: query
        name: sortOrder
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
        description: |
          Sort direction. asc or desc (default desc). Combined with sortBy.
      - in: query
        name: feedId
        schema:
          type: string
          format: uuid
        description: |
          Restrict results to products from this input feed. UUID format.
          Alias feed_id is also accepted. Omit to include all feeds.
      responses:
        '200':
          description: "Paged processed products for the API-key company. Returned after ListProcessedProductsDetailed\
            \ succeeds. Envelope is data[] + meta (page, limit, total, totalPages)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductListResponse"
              example:
                data:
                - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                  product_id: SKU-1001
                  name: Wireless earbuds
                  category: electronics/audio
                  status: completed
                  feed_id: 22222222-2222-2222-2222-222222222222
                  quality_score: 72
                  quality_grade: C
                  created_at: '2026-08-01T10:15:00Z'
                  updated_at: '2026-08-03T14:22:11Z'
                meta:
                  page: 1
                  limit: 25
                  total: 1284
                  totalPages: 52
        '400':
          description: Client-facing catalog validation on filters (for example an invalid feed id). handleV1ListProducts
            uses v1Err validation_error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: invalid feed id
        '500':
          description: Unexpected database/list failure in handleV1ListProducts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
  /products/quality:
    get:
      tags:
      - Products
      summary: List product quality scores
      description: |
        Legacy-compatible quality listing with { data, meta }. Defaults status=completed.
        Optional min_score filters rows after scoring.
      parameters:
      - $ref: "#/components/parameters/Page"
      - $ref: "#/components/parameters/LegacyLimit"
      - in: query
        name: status
        schema:
          type: string
          enum:
          - all
          - needs_review
          - processed
          - completed
          - error
          - processing
          - unprocessed
          default: completed
        description: |
          Filter by product status. Default completed (quality scores are most useful after enrichment).
          all returns every status. Same values as GET /products status.
      - in: query
        name: search
        schema:
          type: string
          maxLength: 200
        description: |
          Free-text search over product name / product_id. Alias q also accepted. Omit to skip.
      - in: query
        name: min_score
        schema:
          type: integer
          minimum: 0
          maximum: 100
        description: |
          Minimum quality score (0-100 inclusive). Rows below this value are excluded after scoring.
          Omit for no score floor.
      - in: query
        name: feedId
        schema:
          type: string
          format: uuid
        description: |
          Restrict to products from this input feed UUID. Alias feed_id also accepted.
      responses:
        '200':
          description: Quality rows for processed products (default status=completed). Returned after
            list+score. min_score filters in-process after scoring. Envelope data + meta (page, limit,
            total).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductQualityListResponse"
              example:
                data:
                - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                  product_id: SKU-1001
                  name: Wireless earbuds
                  quality_score: 72
                  quality_grade: C
                  quality_checks:
                    title: true
                    description: true
                    attributes: false
                meta:
                  page: 1
                  limit: 25
                  total: 410
        '400':
          description: Query min_score is present but not an integer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: invalid min_score
        '500':
          description: Unexpected list failure in handleV1ListProductQuality.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
  /products/reset:
    post:
      tags:
      - Products
      summary: Reset products to unprocessed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - product_ids
              properties:
                product_ids:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    format: uuid
                  description: |
                    Processed (or raw, when kind=raw) product UUIDs to return to unprocessed.
                    Non-empty array of UUID strings.
                kind:
                  type: string
                  enum:
                  - processed
                  - raw
                  default: processed
                  description: |
                    Which table the ids refer to. processed (default) resets enriched products;
                    raw targets raw_products rows instead.
            example:
              product_ids:
              - a1b2c3d4-e5f6-7890-abcd-ef1234567890
              - b2c3d4e5-f6a7-8901-bcde-f12345678901
              kind: processed
      responses:
        '200':
          description: Selected products returned to unprocessed. Returned when ResetProductsToUnprocessed
            commits. Requires company-admin capability (API keys use role api and pass).
          content:
            application/json:
              schema:
                type: object
                required:
                - success
                - reset_count
                - message
                properties:
                  success:
                    type: boolean
                  reset_count:
                    type: integer
                  message:
                    type: string
              example:
                success: true
                reset_count: 12
                message: 12 product(s) returned to unprocessed state
        '400':
          description: Invalid JSON, invalid product_ids UUID, empty product_ids, over max batch, or catalog.ClientError
            from reset.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: product_ids is required
        '403':
          description: Caller role is neither admin nor api (requireCompanyAdmin). Valid company API keys
            use role api and do not hit this.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '500':
          description: Unexpected reset failure after validation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not reset products
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /products/{id}:
    parameters:
    - $ref: "#/components/parameters/ID"
    get:
      tags:
      - Products
      summary: Get processed product
      description: |
        Shared dashboard handler — flat ProcessedProduct JSON (not a legacy { data } envelope).
        List endpoints use { data, meta }.
      responses:
        '200':
          description: Single processed product as a flat object (no data wrapper). Returned when GetProcessedProduct
            finds the id for this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessedProduct"
              example:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                product_id: SKU-1001
                name: Wireless earbuds
                status: completed
        '400':
          description: Path id is not a UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Product not found for this company (or wrong tenant).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    patch:
      tags:
      - Products
      summary: Update processed product
      description: "Flat ProcessedProduct JSON (not a legacy { data } envelope)."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Partial update — only send fields to change
              properties:
                processed_name:
                  type: string
                  description: Enriched display title after AI/manual edit
                processed_description:
                  type: string
                  description: Enriched long description text
                status:
                  type: string
                  enum:
                  - needs_review
                  - processed
                  - completed
                  - error
                  - processing
                  - unprocessed
                  description: Product workflow status. Use completed after accepting enrichment.
                category:
                  type: string
                  description: Category path or unique_id string stored on the product
                attributes:
                  type: object
                  additionalProperties: true
                  description: Source/raw attribute map (string keys to scalar or list values)
                processed_attributes:
                  type: object
                  additionalProperties: true
                  description: Enriched attribute map after processing
            example:
              processed_name: Sony WH-1000XM5 Wireless Noise Cancelling Headphones Black
              status: completed
              attributes:
                color: Black
                brand: Sony
                battery_life_hours: '30'
      responses:
        '200':
          description: Product updated. Flat processed product JSON when UpdateProcessedProduct succeeds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessedProduct"
              example:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                product_id: SKU-1001
                name: Wireless earbuds Pro
                status: completed
        '400':
          description: Invalid JSON, invalid id, or catalog.ClientError / ClientOrLog on update.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not update product
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Resource id not found for this API-key company (or wrong tenant).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /products/process:
    post:
      tags:
      - Processing
      summary: Start processing by EAN (legacy public contract)
      description: |
        Legacy Descrybe public API (handleV1StartProcess). Upserts raw products from
        items[].ean (GTIN), enqueues one processing job, returns HTTP 200 with a data envelope.

        Primary body: items[].ean. Alternate body on the same handler: raw_product_ids
        (raw_products.id UUID list — not GET /products data[].id). Prefer items for public
        integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit
        failures cannot spam catalog writes (HTTP 402 legacy coded envelope).

        Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes.
      requestBody:
        $ref: "#/components/requestBodies/LegacyStartProcessByEAN"
      responses:
        '200':
          description: "Legacy process job accepted. Always HTTP 200 (not 202) with data.process_id when\
            \ enqueue succeeds. Prefer items[].ean; raw_product_ids alternate body is accepted on the\
            \ same path."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyProcessStartEnvelope"
              example:
                data:
                  process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                  status: PENDING
                  processing_type: full
        '400':
          description: Missing body, invalid processing_type, items without ean, invalid raw_product_id,
            empty items, or other validation_error from handleV1StartProcess / v1ErrFromProcessing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: '''items'' array is required'
        '402':
          description: |
            Plan gate blocked starting processing (billing credits/limits/AI/EPREL/feature flags).
            v1ErrFromProcessing maps these to HTTP 402 with a coded legacy envelope.
            Codes: insufficient_credits, product_limit, ai_requires_upgrade, eprel_requires_upgrade,
            plan_gate (message feature_disabled when the platform feature flag is off).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              examples:
                insufficient_credits:
                  summary: Credits
                  value:
                    error:
                      code: insufficient_credits
                      message: Insufficient credits
                feature_disabled:
                  summary: Feature flag
                  value:
                    error:
                      code: plan_gate
                      message: feature_disabled
        "429": { $ref: "#/components/responses/TooManyRequests" }
        '500':
            description: Enqueue or unexpected internal failure starting the legacy job.
            content:
            application/json:
                schema:
                $ref: "#/components/schemas/LegacyAPIError"
                example:
                error:
                    code: internal_server_error
                    message: Internal server error
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /products/process/{id}:
    get:
      tags:
      - Processing
      summary: Processing job status by process_id (legacy public contract)
      description: |
        Poll legacy job status. Path param is process_id from POST /products/process.

        Completed jobs return items[] (EAN-keyed enrichment). In-progress and failed
        jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).

        On COMPLETED items, id is the processed_products UUID (legacy). Additive aliases:
        processed_product_id (same value as id) and raw_product_id (raw_products.id)
        for dual-mode clients that also call raw_product_ids surfaces.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: |
          process_id returned by POST /products/process. UUID format. Required.
      responses:
        '200':
          description: "Legacy job poll. Returned when the job exists for this company. status is uppercase;\
            \ items[] appear when status is COMPLETED."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyProcessStatusEnvelope"
              examples:
                completed:
                  summary: Completed
                  value:
                    data:
                      status: COMPLETED
                      process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                      processing_type: full
                      items:
                      - ean: 0123456789012
                        id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
                        processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
                        raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc
                        status: processed
                        category: electronics
                        title: Acme Wireless Earbuds ANC Black
                      total_items: 1
                      processed_at: '2026-08-04T10:04:12Z'
                processing:
                  summary: In progress
                  value:
                    data:
                      status: PROCESSING
                      process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                      processing_type: full
        '400':
          description: Path id is not a UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: invalid id
        '404':
          description: No processing job with this id for the API-key company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: not_found
                  message: Processing job not found
        '500':
          description: Unexpected failure loading job status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_server_error
                  message: Internal server error
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /categories:
    get:
      tags:
      - Categories
      summary: List categories
      description: "Legacy public contract — { data, meta } with page/limit pagination."
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
        description: |
          1-based page index. Integer, default 1, minimum 1.
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
        description: |
          Page size. Integer, default 25, minimum 1, maximum 100.
      - in: query
        name: search
        schema:
          type: string
          maxLength: 200
        description: |
          Free-text search over category name / unique_id. Alias of q; either may be sent.
      - in: query
        name: q
        schema:
          type: string
          maxLength: 200
        description: |
          Canonical search query (same as search). Omit both to return the full page.
      responses:
        '200':
          description: "Paged categories as data[] + meta. Returned after ListCategories succeeds."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyCategoriesResponse"
              example:
                data:
                - id: 33333333-3333-3333-3333-333333333333
                  unique_id: electronics
                  name: Electronics
                  created_at: '2026-07-01T08:00:00Z'
                  updated_at: '2026-07-15T12:00:00Z'
                meta:
                  page: 1
                  limit: 25
                  total: 42
                  totalPages: 2
        '500':
          description: Unexpected list failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
    post:
      tags:
      - Categories
      summary: Create category
      description: Requires name and unique_id. parent_id is accepted as an alias of parent_unique_id.
      requestBody:
        $ref: "#/components/requestBodies/CreateCategory"
      responses:
        '201':
          description: Category created. HTTP 201 with data containing id, unique_id, name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyCategoryCreateResponse"
              example:
                data:
                  id: 33333333-3333-3333-3333-333333333333
                  unique_id: electronics
                  name: Electronics
        '400':
          description: Invalid JSON, missing name/unique_id, duplicate unique_id, bad parent, or other
            catalog.ClientError from CreateCategory.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not create category
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /categories/create:
    post:
      tags:
      - Categories
      summary: Create category (legacy alias)
      description: "Alias of POST /categories. Same body and { data } response."
      requestBody:
        $ref: "#/components/requestBodies/CreateCategory"
      responses:
        '201':
          description: Category created. HTTP 201 with data containing id, unique_id, name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyCategoryCreateResponse"
              example:
                data:
                  id: 33333333-3333-3333-3333-333333333333
                  unique_id: electronics
                  name: Electronics
        '400':
          description: Invalid JSON, missing name/unique_id, duplicate unique_id, bad parent, or other
            catalog.ClientError from CreateCategory.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not create category
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /categories/{id}:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
      description: |
        For GET/PATCH — category UUID. For DELETE — category unique_id slug (e.g. electronics).
        Required. Format depends on the method.
    get:
      tags:
      - Categories
      summary: Get category by UUID
      description: "Flat category JSON (not a legacy { data } envelope). Path id must be the category\
        \ UUID."
      responses:
        '200':
          description: Category detail (flat CategoryDetail) when found. Path id is typically unique_id
            for v1 delete; shared get handler accepts the mounted id param.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryDetail"
              example:
                id: 33333333-3333-3333-3333-333333333333
                unique_id: electronics
                name: Electronics
        '400':
          description: Invalid category id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Category not found for this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected get failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: get failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    patch:
      tags:
      - Categories
      summary: Update category by UUID
      description: "Flat updated category JSON (not a legacy { data } envelope)."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Partial update — only send fields to change
              properties:
                name:
                  type: string
                  description: Display name shown in the catalog UI
                unique_id:
                  type: string
                  description: |
                    Stable slug identifier (lowercase letters, digits, underscores/hyphens).
                    Changing it may break feed mappings that reference the old id.
                parent_unique_id:
                  type: string
                  nullable: true
                  description: Parent category unique_id, or null for a root category
                description:
                  type: string
                  nullable: true
                  description: Optional human-readable category description
                is_active:
                  type: boolean
                  description: When false, category is hidden from active catalog selection
                position:
                  type: integer
                  description: Sort order among siblings (lower first). Non-negative integer.
            example:
              name: Consumer Electronics
              description: Updated root for consumer devices
              is_active: true
              position: 0
      responses:
        '200':
          description: Category updated (flat object) when PATCH succeeds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryDetail"
              example:
                id: 33333333-3333-3333-3333-333333333333
                unique_id: electronics
                name: Consumer Electronics
        '400':
          description: Invalid JSON or catalog client error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Category not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    delete:
      tags:
      - Categories
      summary: Delete category by unique_id
      description: Path id is the category unique_id (legacy public contract).
      responses:
        '200':
          description: Category deleted by unique_id path param. Returned when DeleteCategoryByUniqueID
            succeeds. Envelope data.message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacySuccessMessage"
              example:
                data:
                  message: Category deleted successfully
        '400':
          description: Empty unique_id or catalog.ClientError blocking delete.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid category id
        '404':
          description: No category with this unique_id for the company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected delete failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: delete failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /attributes:
    get:
      tags:
      - Attributes
      summary: List attributes
      description: "Legacy public contract — { data, meta }."
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
        description: |
          1-based page index. Integer, default 1, minimum 1.
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
        description: |
          Page size. Integer, default 25, minimum 1, maximum 100.
      - in: query
        name: search
        schema:
          type: string
          maxLength: 200
        description: |
          Free-text search over attribute name / key. Alias of q.
      - in: query
        name: q
        schema:
          type: string
          maxLength: 200
        description: |
          Canonical search query (same as search). Omit both for an unfiltered page.
      - in: query
        name: categoryId
        schema:
          type: string
        description: |
          Filter by category unique_id slug (e.g. electronics), not UUID.
      - in: query
        name: sortBy
        schema:
          type: string
          enum:
          - name
          - attributeKey
          - updatedAt
          default: updatedAt
        description: |
          Sort column. name, attributeKey, or updatedAt (default).
      - in: query
        name: sortOrder
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
        description: |
          Sort direction. asc or desc (default desc).
      responses:
        '200':
          description: "Paged attributes as data[] + meta (presentV1Attribute)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAttributesResponse"
              example:
                data:
                - id: 44444444-4444-4444-4444-444444444444
                  key: color
                  name: Color
                  type: text
                  unit: ""
                  required: false
                  category_unique_id: electronics
                  created_at: '2026-07-01T08:00:00Z'
                  updated_at: '2026-07-15T12:00:00Z'
                meta:
                  page: 1
                  limit: 25
                  total: 18
                  totalPages: 1
        '500':
          description: Unexpected list failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
    post:
      tags:
      - Attributes
      summary: Create attribute
      description: Requires name, attribute_key, value_type, and category_unique_id; links the attribute
        to that category.
      requestBody:
        $ref: "#/components/requestBodies/CreateAttribute"
      responses:
        '201':
          description: Attribute created (and linked when category_unique_id resolves). HTTP 201 data
            envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAttributeCreateResponse"
              example:
                data:
                  id: 44444444-4444-4444-4444-444444444444
                  key: color
                  name: Color
                  type: text
                  category_unique_id: electronics
        '400':
          description: Invalid JSON, missing required fields, or catalog.ClientError on create/link.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: "Missing required fields: name, attribute_key, value_type, category_unique_id"
        '404':
          description: Link target category was not found (when link step maps to not found).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected create failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not create attribute
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /attributes/create:
    post:
      tags:
      - Attributes
      summary: Create attribute (legacy alias)
      description: Alias of POST /attributes.
      requestBody:
        $ref: "#/components/requestBodies/CreateAttribute"
      responses:
        '201':
          description: Attribute created (and linked when category_unique_id resolves). HTTP 201 data
            envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAttributeCreateResponse"
              example:
                data:
                  id: 44444444-4444-4444-4444-444444444444
                  key: color
                  name: Color
                  type: text
                  category_unique_id: electronics
        '400':
          description: Invalid JSON, missing required fields, or catalog.ClientError on create/link.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: "Missing required fields: name, attribute_key, value_type, category_unique_id"
        '404':
          description: Link target category was not found (when link step maps to not found).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected create failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not create attribute
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /attributes/{id}:
    parameters:
    - $ref: "#/components/parameters/ID"
    patch:
      tags:
      - Attributes
      summary: Update attribute
      description: Flat attribute JSON (attribute_key / value_type fields — not legacy key/type aliases).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Partial update — only send fields to change
              properties:
                name:
                  type: string
                  description: Human-readable attribute label shown in the UI
                value_type:
                  type: string
                  enum: [string, number, list, multiselect]
                  description: |
                    Value shape for this attribute. string, number, list, or multiselect.
                unit:
                  type: string
                  nullable: true
                  description: Optional unit label (e.g. W, cm). Null clears the unit.
                example:
                  type: string
                  nullable: true
                  description: Sample value for docs/UI hints (e.g. Black)
            example:
              name: Color
              value_type: string
              example: Black
      responses:
        '200':
          description: Attribute updated when PATCH succeeds (flat AttributeDetail / shared handler).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttributeDetail"
              example:
                id: 44444444-4444-4444-4444-444444444444
                attribute_key: color
                name: Colour
        '400':
          description: Invalid JSON or catalog client error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Attribute not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    delete:
      tags:
      - Attributes
      summary: Delete attribute by UUID
      responses:
        '200':
          description: Attribute deleted by UUID. Returned when DeleteAttribute succeeds. data.message
            envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacySuccessMessage"
              example:
                data:
                  message: Attribute deleted successfully
        '400':
          description: Path id is not a UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid attribute id
        '404':
          description: Attribute not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected delete failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: delete failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /feeds:
    get:
      tags:
      - Feeds
      summary: List input feeds
      description: |
        Legacy public contract — { data: Feed[], meta: { page, limit, total } }.
        Accepts page+limit (legacy defaults page=1, limit=25, max 100) or limit+offset.
        Each feed includes presentFeed fields plus dual-support v2 keys (feed_type, sync_interval_minutes, options).
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
        description: |
          1-based page index for legacy page+limit mode. Integer, default 1, minimum 1.
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
        description: |
          Page size. Integer, default 25, minimum 1, maximum 100. Also used with offset.
      - $ref: "#/components/parameters/Offset"
      - in: query
        name: q
        schema:
          type: string
          maxLength: 200
        description: |
          Free-text search over feed name / URL. Canonical search param.
      - in: query
        name: search
        schema:
          type: string
          maxLength: 200
        description: |
          Alias of q. Same free-text search over feed name / URL.
      responses:
        '200':
          description: Paged feeds with meta (including active_total, mapped_total). Returned after Feeds.List.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedListResponse"
              example:
                data:
                - id: 22222222-2222-2222-2222-222222222222
                  name: Main XML feed
                  url: https://supplier.example/feed.xml
                  item_path: products/product
                  is_active: true
                meta:
                  page: 1
                  limit: 25
                  total: 3
                  totalPages: 1
                  offset: 0
                  active_total: 2
                  mapped_total: 1
        '500':
          description: Unexpected list failure (v1Err).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
    post:
      tags:
      - Feeds
      summary: Create feed
      description: |
        Legacy body requires name + item_path (url optional). Dual-support also accepts
        feed_type, sync_interval_minutes, sync_frequency (hours), and multipart file uploads.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name, item_path]
              properties:
                name:
                  type: string
                  description: Display name for this input feed (required)
                item_path:
                  type: string
                  description: |
                    XML item xpath / path stored in options.item_path (e.g. channel/item). Required.
                url:
                  type: string
                  format: uri
                  nullable: true
                  description: |
                    HTTPS (or HTTP) URL of the remote feed. Nullable for upload-only feeds.
                feed_type:
                  type: string
                  enum:
                  - xml
                  - csv
                  description: Source format. xml (default) or csv.
                sync_interval_minutes:
                  type: integer
                  minimum: 1
                  default: 60
                  description: |
                    How often automatic sync should run, in minutes. Default 60. Minimum 1.
                sync_frequency:
                  type: integer
                  minimum: 1
                  description: |
                    Legacy interval in hours. Converted to minutes when sync_interval_minutes is unset.
                is_active:
                  type: boolean
                  description: |
                    When true, status is set to active after create; when false/omitted, typically unmapped.
            example:
              name: Nordic Webshop Google Merchant XML
              item_path: channel/item
              url: https://feeds.example.com/nordic/google-merchant.xml
              feed_type: xml
              sync_interval_minutes: 60
              is_active: true
      responses:
        '201':
          description: Feed created from JSON or multipart. HTTP 201 data envelope when Create succeeds.
            is_active is honored for UUID ids.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedCreateResponse"
              example:
                data:
                  id: 22222222-2222-2222-2222-222222222222
                  name: Main XML feed
                  url: https://supplier.example/feed.xml
                  item_path: products/product
                  is_active: true
        '400':
          description: Invalid JSON/multipart, missing name/item_path (or name+url), upload failure, or
            feeds.ClientError.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: "Missing required fields: name, item_path"
        '500':
          description: Unexpected create failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: could not create feed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /feeds/{id}:
    parameters:
    - $ref: "#/components/parameters/ID"
    get:
      tags:
      - Feeds
      summary: Get feed
      description: "Legacy envelope — { data: Feed }."
      responses:
        '200':
          description: Single feed in data envelope when Feeds.Get succeeds for this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedGetResponse"
              example:
                data:
                  id: 22222222-2222-2222-2222-222222222222
                  name: Main XML feed
                  url: https://supplier.example/feed.xml
                  item_path: products/product
                  is_active: true
        '400':
          description: Path id is not a UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: Invalid id
        '404':
          description: Feed not found for this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: not_found
                  message: Not found
        '500':
          description: Unexpected get failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: get failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    patch:
      tags:
      - Feeds
      summary: Update feed
      description: "Returns flat PresentFeed JSON (same shape as data in GET /feeds/{id}, without the\
        \ data wrapper)."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Partial update — only send fields to change
              properties:
                name:
                  type: string
                  description: Display name for this input feed
                url:
                  type: string
                  format: uri
                  nullable: true
                  description: Remote feed URL, or null to clear
                item_path:
                  type: string
                  description: XML item xpath / path (stored in options.item_path)
                feed_type:
                  type: string
                  enum:
                  - xml
                  - csv
                  description: Source format. xml or csv.
                status:
                  type: string
                  description: |
                    Feed lifecycle status (e.g. active, unmapped, error). Handler validates allowed values.
                sync_interval_minutes:
                  type: integer
                  minimum: 1
                  description: Automatic sync interval in minutes. Minimum 1.
                sync_frequency:
                  type: integer
                  minimum: 1
                  description: |
                    Legacy interval in hours. Converted to minutes when sync_interval_minutes is unset.
            example:
              name: Nordic Webshop Google Merchant XML (EU)
              sync_interval_minutes: 120
              status: active
      responses:
        '200':
          description: Feed updated. Flat PresentFeed from shared handleUpdateFeed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PresentFeed"
              example:
                id: 22222222-2222-2222-2222-222222222222
                name: Main XML feed
                is_active: false
        '400':
          description: Invalid id/JSON or feeds.ClientError.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    delete:
      tags:
      - Feeds
      summary: Delete feed
      description: "Company-admin only. Flat { id, deleted: true } (not a legacy message envelope)."
      responses:
        '200':
          description: Feed deleted. Flat FeedDeleted when handleDeleteFeed succeeds. Requires company-admin
            capability (API keys pass as role api).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedDeleted"
              example:
                id: 22222222-2222-2222-2222-222222222222
                deleted: true
        '400':
          description: Invalid feed UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '403':
          description: requireCompanyAdmin rejected the caller (session non-admin). API keys pass.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected delete failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: delete failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /feeds/{id}/sync:
    post:
      tags:
      - Feeds
      summary: Trigger feed sync
      description: |
        Legacy contract — HTTP 200 { data: { jobId } } (not 202). Enqueues a durable
        feed_sync_jobs row and wakes the worker via NOTIFY; the API process does not
        run sync work. Dual-support also returns job_id. Dashboard POST /api/feeds/{id}/sync
        uses HTTP 202 with a flat job object — do not mix envelopes.
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        "200":
          description: Sync job durably enqueued. Returned when EnqueueSync succeeds. data.jobId (+ job_id alias).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedSyncResponse"
              example:
                data:
                  jobId: 55555555-5555-5555-5555-555555555555
                  job_id: 55555555-5555-5555-5555-555555555555
        '400':
          description: Invalid id or feeds.ClientError (inactive feed, bad source, …).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: Invalid id
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: not_found
                  message: Feed not found
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid id
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: Invalid id
        '500':
          description: Failed to create sync job.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: Failed to create sync job
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '429':
          description: Rate limit exceeded for heavy mutations (30/min/company). Retry-After 60.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: rate limit exceeded

  /feeds/{id}/mappings:
    parameters:
    - $ref: "#/components/parameters/ID"
    get:
      tags:
      - Feeds
      summary: Get feed mappings
      description: "Active feed_mappings row, or { mappings: [] } when none exist."
      responses:
        '200':
          description: Active feed_mappings document for the feed, or an empty mappings payload when none
            exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedMappings"
              example:
                mappings:
                  title: name
                  ean: gtin
        '400':
          description: Invalid feed UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected mappings read failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    put:
      tags:
      - Feeds
      summary: Put feed mappings
      description: Replace active mappings (bumps version).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - mappings
              properties:
                mappings:
                  description: Mapping document (object or array)
            example:
              mappings:
                item_path: channel/item
                fields:
                - source: g:id
                  target: product_id
                - source: title
                  target: name
                - source: g:gtin
                  target: gtin
                - source: g:brand
                  target: brand
                - source: g:image_link
                  target: main_image
      responses:
        '200':
          description: Mappings replaced when PUT body validates and save succeeds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedMappings"
              example:
                mappings:
                  title: name
                  ean: gtin
        '400':
          description: Invalid id/JSON or mapping validation error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /feeds/{id}/extract-schema:
    post:
      tags:
      - Feeds
      summary: Extract feed schema sample paths and preview
      description: Samples the feed source and returns discovered field paths plus a short preview.
      parameters:
      - $ref: "#/components/parameters/ID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                item_path:
                  type: string
                  description: Optional XML item path hint; inferred when omitted
            example:
              item_path: channel/item
      responses:
        '200':
          description: Sample schema/fields extracted from the feed source.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaExtractResult"
              example:
                fields:
                - name
                - gtin
                - price
                item_path: products/product
        '400':
          description: Invalid id or extract client error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected extract failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: extract failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /feeds/{id}/sync-process-sample:
    post:
      tags:
      - Feeds
      summary: Sync feed then process a sample of raw products
      description: |
        Optionally syncs the feed, then starts dashboard-style processing for up to N raw products
        (default 10, max 100). Flat JSON; HTTP 202 when a job is queued, 200 when sync finishes with no products.
      parameters:
      - $ref: "#/components/parameters/ID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                limit:
                  type: integer
                  minimum: 1
                  maximum: 100
                  default: 10
                  description: |
                    Max raw products to queue after sync. Integer, default 10, minimum 1, maximum 100.
                skip_sync:
                  type: boolean
                  default: false
                  description: |
                    When true, skip the feed sync step and process existing raw products only.
                processing_type:
                  type: string
                  default: full
                  description: |
                    Pipeline mode — full (default) or a single step name (category, title, description, attributes).
            example:
              limit: 10
              skip_sync: false
              processing_type: full
      responses:
        '200':
          description: Sample sync finished but produced no raw products to process (sync completed empty).
          content:
            application/json:
              schema:
                type: object
              example:
                ok: true
                imported: 0
                message: no products
        '202':
          description: Sample sync imported products and queued processing (async accept).
          content:
            application/json:
              schema:
                type: object
              example:
                accepted: true
                job_id: 55555555-5555-5555-5555-555555555555
                imported: 5
        '400':
          description: Invalid id or sample validation error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected sample failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: sample failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /export-feeds:
    get:
      tags:
      - Export feeds
      summary: List export feeds
      description: |
        Legacy envelope — { data: ExportFeed[], meta: { page, limit, total } }.
        Accepts page+limit (legacy defaults page=1, limit=25, max 100) or limit+offset.
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          default: 1
          minimum: 1
        description: |
          1-based page index for legacy page+limit mode. Integer, default 1, minimum 1.
      - $ref: "#/components/parameters/LegacyLimit"
      - $ref: "#/components/parameters/Offset"
      responses:
        '200':
          description: "Paged export feeds as data[] + meta (presentV1ExportFeed rows)."
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ExportFeedDetail"
                  meta:
                    type: object
              example:
                data:
                - id: 66666666-6666-6666-6666-666666666666
                  name: Google Shopping
                  format: xml
                  is_active: true
                meta:
                  page: 1
                  limit: 25
                  total: 1
        '500':
          description: Unexpected list failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
    post:
      tags:
      - Export feeds
      summary: Create export feed
      description: "Legacy envelope — { data: ExportFeed }."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - format
              properties:
                name:
                  type: string
                  description: Display name for the export feed (required)
                format:
                  type: string
                  enum:
                  - xml
                  - csv
                  description: Output format. xml or csv (required).
                source_feed_id:
                  type: string
                  format: uuid
                  description: Optional input feed UUID this export is derived from
                template:
                  type: object
                  description: |
                    Export template document (root/item/mappings). Structure depends on format.
                structure:
                  type: object
                  description: Legacy alias for template — same object shape
                mappings:
                  type: object
                  description: Field mapping object (source → target). May also live under template.
                filters:
                  type: object
                  description: |
                    Product filters applied at generate time (e.g. statuses list). Object map.
                root_xpath:
                  type: string
                  description: Optional XML root xpath hint for template builders
                item_xpath:
                  type: string
                  description: Optional XML item xpath hint for template builders
            example:
              name: Warehouse Inventory CSV
              format: csv
              source_feed_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              filters:
                statuses:
                - completed
              mappings:
                title: processed_name
                gtin: gtin
      responses:
        '201':
          description: Export feed created when name+format validate. HTTP 201 data envelope.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    $ref: "#/components/schemas/ExportFeedDetail"
              example:
                data:
                  id: 66666666-6666-6666-6666-666666666666
                  name: Google Shopping
                  format: xml
        '400':
          description: "Invalid JSON, missing name/format, or feeds.ClientError. Note: ClientOrLog fallback\
            \ also uses flat Error on some paths."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: "Missing required fields: name, format"
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /export-feeds/{id}:
    parameters:
    - $ref: "#/components/parameters/ID"
    get:
      tags:
      - Export feeds
      summary: Get export feed
      description: Flat export feed row including template/filters (not the list presentV1ExportFeed /
        data envelope).
      responses:
        '200':
          description: Export feed detail when found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportFeedDetail"
              example:
                id: 66666666-6666-6666-6666-666666666666
                name: Google Shopping
                format: xml
        '400':
          description: Invalid UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected get failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: get failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    patch:
      tags:
      - Export feeds
      summary: Update export feed
      description: Flat updated export feed row.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Partial update — only send fields to change
              properties:
                name:
                  type: string
                  description: Display name for the export feed
                is_active:
                  type: boolean
                  description: When false, public token URLs may still exist but feed is inactive
                template:
                  type: object
                  description: Export template document (root/item/mappings)
                filters:
                  type: object
                  description: Product filters applied at generate time
            example:
              name: Google Shopping XML EU
              is_active: true
              filters:
                statuses:
                - completed
      responses:
        '200':
          description: Export feed updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportFeedDetail"
              example:
                id: 66666666-6666-6666-6666-666666666666
                name: Google Shopping EU
                format: xml
        '400':
          description: Invalid JSON or client error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
    delete:
      tags:
      - Export feeds
      summary: Delete export feed
      description: "Company-admin only. Flat { status: ok } (not LegacySuccessMessage)."
      responses:
        '200':
          description: Export feed deleted (admin capability; API keys pass). Flat deleted marker.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedDeleted"
              example:
                id: 66666666-6666-6666-6666-666666666666
                deleted: true
        '400':
          description: Invalid UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '403':
          description: requireCompanyAdmin rejected session non-admin.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected delete failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: delete failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /export-feeds/{id}/template:
    put:
      tags:
      - Export feeds
      summary: Update export feed template
      description: Updates template and/or filters; returns flat ExportFeedDetail.
      parameters:
      - $ref: "#/components/parameters/ID"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                template:
                  type: object
                  description: |
                    Full export template document (root, item path, field mappings). Replaces prior template when set.
                filters:
                  type: object
                  description: |
                    Product filters for generation (e.g. statuses). Replaces prior filters when set.
            example:
              template:
                root: rss
                item: channel/item
                mappings:
                  title: processed_name
                  gtin: gtin
              filters:
                statuses:
                - completed
      responses:
        '200':
          description: Template saved for the export feed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportFeedDetail"
              example:
                id: 66666666-6666-6666-6666-666666666666
                name: Google Shopping
                format: xml
        '400':
          description: Invalid id/body or template validation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /export-feeds/{id}/rotate-token:
    post:
      tags:
      - Export feeds
      summary: Rotate public export URL token
      description: |
        Replaces public_token so the previous /api/public/export-feeds/{token}.{xml|csv}
        URL stops working immediately. Requires company admin (or platform staff).
        Response is a flat ExportFeedDetail including the new public_token.
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '200':
          description: Token rotated; body includes the new public_token and URLs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportFeedDetail"
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
        '403':
          description: Admin required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        "400": { $ref: "#/components/responses/BadRequest" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "500": { $ref: "#/components/responses/InternalServerError" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /export-feeds/{id}/generate:
    post:
      tags:
      - Export feeds
      summary: Generate export feed file
      description: |
        Legacy envelope — { data: { generated, format, filePath, downloadUrl, ... } }.
        Content is streamed live via public download URL (not persisted to disk).
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '200':
          description: Generation finished. data includes generated, format, downloadUrl, products_exported,
            status. filePath may be null — documented under examples.value for RapiDoc safety.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: object
              examples:
                ok:
                  summary: Generated
                  value:
                    data:
                      generated: true
                      format: xml
                      filePath: null
                      downloadUrl: /api/public/export-feeds/tok_abc.xml
                      products_exported: 1284
                      last_generated_at: '2026-08-04T12:00:00Z'
                      status: ready
        '400':
          description: Invalid id or generation_failed client error (empty template, …).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: generation_failed
                  message: template is empty
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: not_found
                  message: Export feed not found
        '500':
          description: Generation failed internally.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: generation_failed
                  message: Failed to generate export feed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /export-feeds/{id}/export-products:
    post:
      tags:
      - Export feeds
      summary: Export selected processed products
      description: |
        Streams the rendered export for the given processed product UUIDs.
        Response body is CSV or XML bytes (not JSON). Headers include Content-Disposition and X-Products-Exported.
      parameters:
      - $ref: "#/components/parameters/ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - product_ids
              properties:
                product_ids:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    format: uuid
                  description: |
                    Processed product UUIDs to include in the streamed export. Non-empty array.
            example:
              product_ids:
              - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f
              - 550e8400-e29b-41d4-a716-446655440001
      responses:
        '200':
          description: Selected products exported / file bytes produced for this export feed.
          content:
            application/json:
              schema:
                type: object
              example:
                exported: 25
                skipped: 2
        '400':
          description: Invalid id/body or selection validation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '404':
          description: Export feed not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '500':
          description: Unexpected export failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: export failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /campaigns:
    get:
      tags:
      - Campaigns
      summary: List seasonal campaign presets (legacy alias)
      description: |
        Alias of GET /marketing/calendar. Returns legacy envelope
        { data: { year, presets, prepared } }. Not email campaigns.
      parameters:
      - name: year
        in: query
        schema:
          type: integer
          example: 2026
          minimum: 2000
          maximum: 2100
        description: |
          Calendar year for seasonal presets (e.g. 2026). Integer. When omitted, server uses the current year.
      responses:
        '200':
          description: Legacy alias of marketing calendar. data envelope around calendar payload (handleV1ListCampaigns
            → v1OK).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketingCalendar"
              example:
                data:
                  year: 2026
                  presets:
                  - id: back_to_school
        '400':
          description: Calendar query validation failed (v1MarketingCalendar → v1Err).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: invalid year
        '500':
          description: Unexpected calendar failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: internal_error
                  message: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
  /campaigns/prepare:
    post:
      tags:
      - Campaigns
      summary: Prepare seasonal campaign export (legacy alias)
      description: |
        Alias of POST /marketing/calendar/prepare. Legacy envelope
        { data: { preset_id, name, export_feed_id, created, ... } }.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - preset_id
              properties:
                preset_id:
                  type: string
                  enum:
                  - black_friday
                  - christmas
                  description: |
                    Seasonal preset id. black_friday or christmas (required).
                year:
                  type: integer
                  minimum: 2000
                  maximum: 2100
                  description: |
                    Target calendar year for date windows. Integer. Defaults to current year when omitted.
                format:
                  type: string
                  enum:
                  - csv
                  - xml
                  default: csv
                  description: Export feed format for the prepared campaign. csv (default) or xml.
                force_new:
                  type: boolean
                  description: |
                    When true, create a new export feed even if one already exists for this preset/year.
            example:
              preset_id: black_friday
              year: 2026
              format: csv
              force_new: false
      responses:
        '200':
          description: Prepare reused existing campaign. data envelope (PreparedCampaignEnvelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreparedCampaignEnvelope"
              example:
                data:
                  preset_id: back_to_school
                  year: 2026
                  created: false
        '201':
          description: Prepare created a new campaign. data envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreparedCampaignEnvelope"
              example:
                data:
                  preset_id: back_to_school
                  year: 2026
                  created: true
        '400':
          description: Invalid JSON or marketing.ClientError (v1Err). ClientOrLog may emit flat Error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: validation_error
                  message: invalid json
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /marketing/calendar:
    get:
      tags:
      - Campaigns
      summary: List seasonal campaign presets
      description: Canonical path (flat JSON). Prefer /campaigns for legacy clients.
      parameters:
      - name: year
        in: query
        schema:
          type: integer
          example: 2026
          minimum: 2000
          maximum: 2100
        description: |
          Calendar year for seasonal presets (e.g. 2026). Integer. When omitted, server uses the current year.
      responses:
        '200':
          description: Flat marketing calendar payload (year, presets, prepared) when query validates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketingCalendar"
              example:
                year: 2026
                presets:
                - id: back_to_school
                  label: Back to school
        '400':
          description: Invalid query (year/preset).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid date
        '500':
          description: Unexpected calendar failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
  /marketing/calendar/prepare:
    post:
      tags:
      - Campaigns
      summary: Prepare seasonal campaign export
      description: Canonical path (flat JSON). Prefer /campaigns/prepare for legacy clients.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - preset_id
              properties:
                preset_id:
                  type: string
                  enum:
                  - black_friday
                  - christmas
                  description: |
                    Seasonal preset id. black_friday or christmas (required).
                year:
                  type: integer
                  minimum: 2000
                  maximum: 2100
                  description: |
                    Target calendar year for date windows. Integer. Defaults to current year when omitted.
                format:
                  type: string
                  enum:
                  - csv
                  - xml
                  description: Export feed format for the prepared campaign. csv or xml.
                force_new:
                  type: boolean
                  description: |
                    When true, create a new export feed even if one already exists for this preset/year.
            example:
              preset_id: black_friday
              year: 2026
              format: csv
              force_new: false
      responses:
        '200':
          description: Prepare reused an existing campaign (Created=false). Flat PreparedCampaign JSON.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreparedCampaign"
              example:
                preset_id: back_to_school
                year: 2026
                created: false
        '201':
          description: Prepare created a new campaign (Created=true). Flat PreparedCampaign JSON.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreparedCampaign"
              example:
                preset_id: back_to_school
                year: 2026
                created: true
        '400':
          description: Invalid JSON or marketing.ClientError.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /process:
    get:
      tags:
      - Processing
      summary: List processing jobs (raw_product_ids surface)
      description: |
        Flat job list for the /process + raw_product_ids contract.
        Separate from legacy GET /products/process/{id}.
      parameters:
      - $ref: "#/components/parameters/Limit"
      responses:
        '200':
          description: "Recent dashboard-style jobs. Flat object with jobs[] and limit (handleV1ListProcessJobs\
            \ — not legacy data envelope)."
          content:
            application/json:
              schema:
                type: object
                required:
                - jobs
                - limit
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProcessingJob"
                  limit:
                    type: integer
              example:
                jobs:
                - id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                  status: processing
                  total_products: 25
                  processed_products: 8
                limit: 50
        '500':
          description: Unexpected job list failure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
    post:
      tags:
      - Processing
      summary: Start processing job by raw_product_ids
      description: |
        Enqueues AI processing for existing raw product UUIDs.
        Same pipeline as dashboard POST /api/processing/jobs.
        Flat JSON body/response (no data wrapper); HTTP 202.
        Pass raw_products.id only (GET /products data[].raw_product_id), never PresentProduct.id.

        Separate from legacy POST /products/process (items[].ean → 200 { data }).
        Plan gates return HTTP 402 PlanGateError (not the legacy coded envelope).
      requestBody:
        $ref: "#/components/requestBodies/StartProcessByRawIDs"
      responses:
        '202':
          description: "Job accepted (HTTP 202). Flat ProcessingJobStartResponse; may include jobs[] when\
            \ auto-split."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJobStartResponse"
              example:
                id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                status: pending
                total_products: 2
                processing_type: full
        '400':
          description: Invalid JSON, invalid raw_product_id, empty ids, or processing.ClientError / LogAndError.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not start processing job
        '402':
          description: Plan gate blocked start. Special shape from handleStartProcessingJob (error string
            + code + upgrade_url) — not the legacy coded envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlanGateError"
              example:
                error: Insufficient credits
                code: insufficient_credits
                upgrade_url: /pricing
        "429": { $ref: "#/components/responses/TooManyRequests" }
        '500':
            description: River enqueue failed after job row create.
            content:
            application/json:
                schema:
                $ref: "#/components/schemas/FlatAPIError"
                example:
                error: enqueue failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /process/{id}:
    get:
      tags:
      - Processing
      summary: Processing job status (raw_product_ids surface)
      description: |
        Flat ProcessingJob JSON. Not the legacy { data: { process_id, items } } envelope.
        When status is completed (finished), response is additively enriched with items[]
        and total_items (same processed product projection as GET /products/process/{id}).
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '200':
          description: "Flat ProcessingJob when GetJob succeeds for this company. Completed jobs include items[]."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJob"
              examples:
                processing:
                  summary: In progress
                  value:
                    id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                    status: processing
                    total_products: 25
                    processed_products: 8
                    processing_type: full
                    current_step: title
                completed:
                  summary: Finished with processed products
                  value:
                    id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                    status: completed
                    total_products: 1
                    processed_products: 1
                    processing_type: full
                    items:
                    - ean: '0123456789012'
                      id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
                      status: processed
                      title: Acme Wireless Earbuds ANC Black
                    total_items: 1
        '400':
          description: Path id is not a UUID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid id
        '404':
          description: Job not found for this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /process/{id}/cancel:
    post:
      tags:
      - Processing
      summary: Cancel processing job
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '200':
          description: Cancel/terminate acknowledged. Returns the updated job object when CancelJob succeeds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJob"
              example:
                id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                status: cancelled
                total_products: 25
        '400':
          description: Invalid id, or processing.ClientError / LogAndError (job not cancellable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not cancel job
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Resource id not found for this API-key company (or wrong tenant).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /process/{id}/terminate:
    post:
      tags:
      - Processing
      summary: Terminate processing job (alias of cancel)
      description: "Pixel/legacy naming alias of POST /process/{id}/cancel."
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '200':
          description: Cancel/terminate acknowledged. Returns the updated job object when CancelJob succeeds.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJob"
              example:
                id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                status: cancelled
                total_products: 25
        '400':
          description: Invalid id, or processing.ClientError / LogAndError (job not cancellable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not cancel job
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '500':
          description: Unexpected server error for this operation (handler internal_error / flat Error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: internal error
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Resource id not found for this API-key company (or wrong tenant).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /process/{id}/retry:
    post:
      tags:
      - Processing
      summary: Retry a failed or cancelled processing job
      parameters:
      - $ref: "#/components/parameters/ID"
      responses:
        '202':
          description: Retry accepted (HTTP 202). Flat job after RetryJob + enqueue.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingJob"
              example:
                id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
                status: pending
                total_products: 25
        '400':
          description: Invalid id or job not retryable (ClientError / LogAndError).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: could not retry job
        "429": { $ref: "#/components/responses/TooManyRequests" }
        '500':
            description: Enqueue failed after retry.
            content:
            application/json:
                schema:
                $ref: "#/components/schemas/FlatAPIError"
                example:
                error: enqueue failed
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '403':
          description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually
            return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin
            callers (API keys use role api and pass).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '404':
          description: Resource id not found for this API-key company (or wrong tenant).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '422':
          description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat
            or coded). 422 is documented for clients that expect an explicit validation status; body matches
            FlatAPIError or LegacyAPIError.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/FlatAPIError"
                - $ref: "#/components/schemas/LegacyAPIError"
              examples:
                flat:
                  summary: Flat Error()
                  value:
                    error: invalid json
                legacy:
                  summary: v1Err coded
                  value:
                    error:
                      code: validation_error
                      message: invalid json
  /team/{userID}:
    parameters:
    - name: userID
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Membership user id within the selected company
    patch:
      tags:
      - Team
      summary: Update team member role
      description: |
        Company admin or platform admin. Demoting the last active admin returns 409.
        Requires session cookie + CSRF double-submit (X-CSRF-Token).
        Dashboard surface under /api (not the public /api/v1 API-key base).
      security:
      - SessionCookie: []
        CSRFHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - role
              properties:
                role:
                  type: string
                  enum:
                  - admin
                  - member
                  description: Normalized to admin|member (case-insensitive input accepted)
            example:
              role: member
      responses:
        '200':
          description: Role updated (or unchanged)
          content:
            application/json:
              schema:
                type: object
                required:
                - status
                - role
                - user_id
                properties:
                  status:
                    type: string
                    example: ok
                  role:
                    type: string
                    enum:
                    - admin
                    - member
                  user_id:
                    type: string
                    format: uuid
              example:
                status: ok
                role: member
                user_id: 4f3c2b1a-0e9d-4c8b-7a6f-5e4d3c2b1a09
        '400':
          description: Invalid request for this route (HTTP 400).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '403':
          description: Forbidden (not company/platform admin)
        '404':
          description: Resource not found (HTTP 404).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: not found
        '409':
          description: Cannot demote the last admin
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: cannot demote the last admin
        '401':
          description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer
            and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy
            coded envelope (not a flat string).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LegacyAPIError"
              example:
                error:
                  code: unauthorized
                  message: Unauthorized
        '422':
          description: Validation failed. Dashboard routes may use this status; prefer reading the message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: validation failed
        '500':
          description: Unexpected server error (HTTP 500).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
  /admin/emails/set-password:
    post:
      tags:
      - Admin
      summary: Re-issue set-password invites
      description: |
        Platform admin only. Prefers durable invite reissue (ReissueSetPasswordInvite);
        falls back to HMAC set-password tokens when the user has no active membership.
        Skips synthetic …@legacy.local emails. Rate-limited per admin.
        When SMTP is disabled and a single user_id is provided, the response may include
        a one-time token for local/staging link copy (never logs email/token).
        Dashboard surface under /api (not the public /api/v1 API-key base).
      security:
      - SessionCookie: []
        CSRFHeader: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                user_id:
                  type: string
                  format: uuid
                  description: |
                    Target user UUID. When omitted, bulk-targets users needing a password (capped).
            example:
              user_id: 4f3c2b1a-0e9d-4c8b-7a6f-5e4d3c2b1a09
      responses:
        '200':
          description: Issue/send summary
          content:
            application/json:
              schema:
                type: object
                required:
                - sent
                - issued
                - skipped
                - smtp_enabled
                - mode
                properties:
                  sent:
                    type: integer
                  issued:
                    type: integer
                  skipped:
                    type: integer
                  skipped_synthetic:
                    type: integer
                  skipped_ineligible:
                    type: integer
                  skipped_rate_limited:
                    type: integer
                  skipped_send:
                    type: integer
                  smtp_enabled:
                    type: boolean
                  mode:
                    type: string
                    example: invite
                  token:
                    type: string
                    description: Present only for single-user reissue when SMTP is off
              example:
                sent: 1
                issued: 1
                skipped: 0
                skipped_synthetic: 0
                skipped_ineligible: 0
                skipped_rate_limited: 0
                skipped_send: 0
                smtp_enabled: true
                mode: invite
        '401':
          description: Unauthorized — missing session or privilege for this dashboard route.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: unauthorized
        '429':
          description: Rate limit exceeded for this admin action. May include Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: Too many requests
        '503':
          description: Dependency unavailable (for example mailer down).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: mailer unavailable
        '400':
          description: Invalid request for this route (HTTP 400).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: invalid json
        '403':
          description: Forbidden for this route (HTTP 403).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: admin required
        '409':
          description: Conflict with current resource state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: conflict
        '422':
          description: Validation failed. Dashboard routes may use this status; prefer reading the message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: validation failed
        '500':
          description: Unexpected server error (HTTP 500).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlatAPIError"
              example:
                error: list failed
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key (dk_...)
      description: |
        Preferred scheme for /api/v1. Value is the raw company API key
        (prefix dk_...), not a JWT. Example header:

          Authorization: Bearer dk_your_key

        Create keys in Settings -> API keys. RapiDoc Try-it: paste the key, or
        use "Use my API key" when logged into the docs page. Full keys are never
        published in this YAML.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Alternate scheme for /api/v1. Same company API key as BearerAuth, sent
        as header X-API-Key: dk_your_key. When both Authorization Bearer and
        X-API-Key are present, Bearer wins.
    SessionCookie:
      type: apiKey
      in: cookie
      name: descrybe_session
      description: Dashboard session cookie only (SESSION_COOKIE_NAME; default descrybe_session). Not
        valid for /api/v1 public routes.
    CSRFHeader:
      type: apiKey
      in: header
      name: X-CSRF-Token
      description: Dashboard CSRF double-submit header (must match descrybe_csrf cookie). Not used by
        /api/v1 API-key routes.
  parameters:
    ID:
      in: path
      name: id
      required: true
      schema:
        type: string
        format: uuid
      description: |
        Resource UUID for this path (product, attribute, feed, export feed, or processing job).
        Format: UUID string (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Required.
    Limit:
      in: query
      name: limit
      schema:
        type: integer
        default: 50
        maximum: 200
        minimum: 1
      description: |
        Page size for non-legacy list endpoints. Integer, default 50, minimum 1, maximum 200.
    LegacyLimit:
      in: query
      name: limit
      schema:
        type: integer
        default: 25
        maximum: 100
        minimum: 1
      description: |
        Page size for legacy public list endpoints. Integer, default 25, minimum 1, maximum 100.
    Page:
      in: query
      name: page
      schema:
        type: integer
        default: 1
        minimum: 1
      description: |
        1-based page index for offset/page pagination. Integer, default 1, minimum 1.
    Offset:
      in: query
      name: offset
      schema:
        type: integer
        default: 0
      description: Offset pagination (ignored when cursor or after_id is set)
    Cursor:
      in: query
      name: cursor
      schema:
        type: string
      description: Opaque keyset cursor from next_cursor (preferred for deep pages)
    AfterID:
      in: query
      name: after_id
      schema:
        type: string
        format: uuid
      description: Keyset bookmark by product id; cursor wins when both are set
  requestBodies:
    LegacyStartProcessByEAN:
      required: true
      description: |
        Prefer items[].ean (legacy public). Same handler also accepts raw_product_ids
        when items is omitted. Sending neither returns validation_error.
        raw_product_ids must be raw_products.id values (see GET /products data[].raw_product_id).
        Do not pass PresentProduct.id / processed_products.id.
      content:
        application/json:
          schema:
            type: object
            properties:
              items:
                type: array
                minItems: 1
                description: Primary legacy body — required unless raw_product_ids is set
                items:
                  type: object
                  required:
                  - ean
                  properties:
                    ean:
                      type: string
                      description: GTIN / EAN barcode digits (required). Typically 8-14 characters.
                    category_unique_id:
                      type: string
                      description: Optional category unique_id; forces categorization when set
                    title:
                      type: string
                      description: Optional product title seed for enrichment
                    description:
                      type: string
                      description: Optional long description seed for enrichment
                    specifications:
                      type: array
                      description: Optional key/value specification pairs for attribute hints
                      items:
                        type: object
                        properties:
                          key:
                            type: string
                            description: Specification attribute key
                          value:
                            type: string
                            description: Specification attribute value
                    search:
                      type: string
                      description: Optional search keywords passed into enrichment context
                    main_image:
                      type: string
                      format: uri
                      description: Primary product image URL
                    more_images:
                      description: Additional image URLs as a single string or string array
                      oneOf:
                      - type: string
                      - type: array
                        items:
                          type: string
                    image_url:
                      type: string
                      format: uri
                      description: Alternate primary image URL field (alias of main_image)
                    additional_image_urls:
                      description: Extra image URLs as a single string or string array
                      oneOf:
                      - type: string
                      - type: array
                        items:
                          type: string
                    image_link:
                      type: string
                      format: uri
                      description: Google-style primary image_link URL
                    additional_image_link:
                      description: Google-style additional image links (string or array)
                      oneOf:
                      - type: string
                      - type: array
                        items:
                          type: string
              raw_product_ids:
                type: array
                items:
                  type: string
                  format: uuid
                minItems: 1
                description: |
                  Alternate body — existing raw_products.id UUIDs (items takes precedence).
                  Not PresentProduct.id. Use GET /products data[].raw_product_id.
              processing_type:
                description: |
                  full (default); legacy steps category|title|description|attributes (string or
                  array of those steps); dual-mode also accepts v2 dashboard types such as
                  normalize_only, enhance_only, attributes_only, eprel_only, categorize_only
                  (ParseV1ProcessingType). Alias processingType accepted when values match.
                oneOf:
                - type: string
                  enum:
                  - full
                  - category
                  - title
                  - description
                  - attributes
                  - normalize_only
                  - enhance
                  - enhance_only
                  - enhance-only
                  - attributes_only
                  - specs
                  - specifications
                  - eprel
                  - eprel_only
                  - categorize
                  - categorize_only
                  - categorize_enhance
                - type: array
                  items:
                    type: string
                    enum:
                    - category
                    - title
                    - description
                    - attributes
              processingType:
                description: CamelCase alias of processing_type (must match if both set)
                oneOf:
                - type: string
                - type: array
                  items:
                    type: string
              processing_types:
                type: array
                items:
                  type: string
                description: Dashboard fine-grained steps; used when processing_type omitted
          examples:
            by_ean:
              summary: "Legacy items[].ean (preferred)"
              value:
                items:
                - ean: '4548736132174'
                  title: WH-1000XM5 Sony WH-1000XM5 Black
                  category_unique_id: electronics
                  main_image: https://images.example.com/products/wh1000xm5-black.jpg
                  more_images:
                  - https://images.example.com/products/wh1000xm5-black-side.jpg
                - ean: 0194252092942
                  title: Anker PowerLine III USB-C to USB-C 2m
                processing_type: full
            by_raw_ids:
              summary: Alternate raw_product_ids on same path
              value:
                raw_product_ids:
                - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f
                processing_type: normalize_only
    StartProcessByRawIDs:
      required: true
      description: |
        Dashboard-style start body — existing raw product UUIDs only (no items[].ean).
        Used by POST /process. Returns flat ProcessingJob JSON with HTTP 202.
        IDs must be raw_products.id (GET /products data[].raw_product_id), never PresentProduct.id.
      content:
        application/json:
          schema:
            type: object
            required:
            - raw_product_ids
            properties:
              raw_product_ids:
                type: array
                items:
                  type: string
                  format: uuid
                minItems: 1
                description: |
                  raw_products.id UUIDs to enqueue. Non-empty array of UUID strings (required).
                  Do not pass processed product list id values.
              processing_type:
                type: string
                description: |
                  full (default) or a legacy/dashboard step (category, title, description,
                  attributes, normalize_only, enhance_only, …).
                default: full
                example: full
              processing_types:
                type: array
                items:
                  type: string
                description: Optional fine-grained steps (dashboard); StartJob uses processing_type
          example:
            raw_product_ids:
            - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f
            - 550e8400-e29b-41d4-a716-446655440001
            processing_type: full
    CreateCategory:
      required: true
      description: |
        Create a catalog category. Requires name and unique_id. parent_id is accepted as an
        alias of parent_unique_id.
      content:
        application/json:
          schema:
            type: object
            required:
            - name
            - unique_id
            properties:
              name:
                type: string
                description: Display name shown in the catalog UI (required)
              unique_id:
                type: string
                description: |
                  Stable slug identifier (e.g. headphones). Lowercase letters, digits,
                  underscores/hyphens. Required and unique within the company.
              parent_unique_id:
                type: string
                nullable: true
                description: Parent category unique_id, or null/omit for a root category
              parent_id:
                type: string
                nullable: true
                description: Legacy alias of parent_unique_id
              description:
                type: string
                nullable: true
                description: Optional human-readable category description
          example:
            name: Headphones
            unique_id: electronics_audio_headphones
            parent_id: electronics_audio
            description: Over-ear and in-ear headphones
    CreateAttribute:
      required: true
      description: |
        Create a catalog attribute and link it to a category. Requires name, attribute_key,
        value_type, and category_unique_id.
      content:
        application/json:
          schema:
            type: object
            required:
            - name
            - attribute_key
            - value_type
            - category_unique_id
            properties:
              name:
                type: string
                description: Human-readable attribute label (required)
              attribute_key:
                type: string
                description: |
                  Stable machine key (e.g. battery_life_hours). Snake_case preferred. Required.
              value_type:
                type: string
                enum:
                - string
                - number
                - list
                - multiselect
                description: |
                  Value shape. string, number, list, or multiselect (required).
              category_unique_id:
                type: string
                description: Category unique_id slug to attach this attribute to (required)
              unit:
                type: string
                nullable: true
                description: Optional unit label (e.g. W, cm)
              example:
                type: string
                nullable: true
                description: Sample value for docs/UI hints
              required:
                type: boolean
                default: false
                description: When true, products in this category should supply the attribute
              parent_key:
                type: string
                nullable: true
                description: Optional parent attribute_key for nested/grouped attributes
          example:
            name: Battery Life
            attribute_key: battery_life_hours
            value_type: number
            category_unique_id: electronics_audio_headphones
            unit: h
            required: false
            example: '65'
  schemas:
    HealthStatus:
      type: object
      required:
      - status
      - service
      - maintenance
      - read_only
      properties:
        status:
          type: string
          example: ok
          description: ok for liveness; ready/not_ready on /readyz
        service:
          type: string
          example: api
        maintenance:
          type: boolean
          description: When true
          API is in maintenance mode: null
        read_only:
          type: boolean
          description: When true
          mutating writes are rejected: null
        hypercare:
          type: boolean
          description: When true, tenant hypercare report-missing CTA is shown (P1-17)
        checks:
          type: object
          additionalProperties:
            type: string
          description: Present on /readyz (e.g. database ok|fail|unavailable)
        error:
          type: string
          description: Present on /readyz when not ready (safe public message)
    ProductListResponse:
        type: object
        required: [data, meta]
        properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PresentProduct"
        meta:
          type: object
          required:
          - page
          - limit
          - total
          properties:
            page:
              type: integer
            limit:
              type: integer
            total:
              type: integer
            totalPages:
              type: integer
    ProductQualityListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              product_id:
                type: string
              name:
                type: string
              quality_score:
                type: integer
              quality_grade:
                type: string
              quality_checks:
                type: object
                additionalProperties: true
        meta:
          type: object
          required:
          - page
          - limit
          - total
          properties:
            page:
              type: integer
            limit:
              type: integer
            total:
              type: integer
    PresentProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: |
            processed_products.id for this list row. Do not send as raw_product_ids —
            use raw_product_id instead.
        product_id:
          type: string
          example: SONY-WH1000XM5-B
        name:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        status:
          type: string
          example: completed
        raw_product_id:
          type: string
          format: uuid
          nullable: true
          description: |
            raw_products.id for dual-mode POST /products/process and POST /process bodies.
            Prefer this over id when starting jobs by UUID.
        feed_id:
          type: string
          format: uuid
          nullable: true
        quality_score:
          type: integer
        quality_grade:
          type: string
          example: C
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ProcessedProduct:
      type: object
      properties:
        id:
          type: string
          format: uuid
        product_id:
          type: string
          example: SONY-WH1000XM5-B
        name:
          type: string
        processed_name:
          type: string
        category:
          type: string
        description:
          type: string
        processed_description:
          type: string
        status:
          type: string
          example: completed
        raw_product_id:
          type: string
          format: uuid
        feed_id:
          type: string
          format: uuid
        gtin:
          type: string
        attributes:
          type: object
          additionalProperties: true
        processed_attributes:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CategoryListResponse:
      type: object
      required:
      - categories
      - total
      - limit
      - offset
      properties:
        categories:
          type: array
          items:
            type: object
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    LegacyPaginationMeta:
      type: object
      required:
      - page
      - limit
      - total
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
    LegacyCategoriesResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/LegacyCategory"
        meta:
          $ref: "#/components/schemas/LegacyPaginationMeta"
    LegacyCategory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        unique_id:
          type: string
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CategoryDetail:
      type: object
      description: "Flat category row from GET/PATCH /categories/{id} (not a legacy data envelope)."
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        unique_id:
          type: string
        parent_unique_id:
          type: string
          nullable: true
        path:
          type: string
          nullable: true
        level:
          type: integer
        position:
          type: integer
        is_active:
          type: boolean
        description:
          type: string
          nullable: true
        title_template:
          nullable: true
        description_template:
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LegacyCategoryCreateResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - id
          - unique_id
          - name
          properties:
            id:
              type: string
              format: uuid
            unique_id:
              type: string
            name:
              type: string
    LegacyAttributesResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/LegacyAttribute"
        meta:
          $ref: "#/components/schemas/LegacyPaginationMeta"
    LegacyAttribute:
      type: object
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
        name:
          type: string
        type:
          type: string
        unit:
          type: string
          nullable: true
        required:
          type: boolean
        category_unique_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AttributeDetail:
      type: object
      description: "Flat attribute row from PATCH /attributes/{id} (not a legacy data envelope)."
      properties:
        id:
          type: string
          format: uuid
        attribute_key:
          type: string
        name:
          type: string
        value_type:
            type: string
            enum: [string, number, list, multiselect]
        unit:
            type: string
            nullable: true
        example:
            type: string
            nullable: true
        parent_key:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LegacyAttributeCreateResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            key:
              type: string
            name:
              type: string
            type:
              type: string
            unit:
              type: string
              nullable: true
            category_unique_id:
              type: string
            required:
              type: boolean
    LegacySuccessMessage:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - message
          properties:
            message:
              type: string
    FeedListResponse:
      type: object
      required:
      - data
      - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PresentFeed"
        meta:
          type: object
          required:
          - page
          - limit
          - total
          properties:
            page:
              type: integer
            limit:
              type: integer
            total:
              type: integer
              description: All matching feeds
            offset:
              type: integer
            active_total:
              type: integer
              description: Feeds with status active (truly syncing)
            mapped_total:
              type: integer
              description: Feeds with status mapped (fields saved, not activated)
    FeedGetResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: "#/components/schemas/PresentFeed"
    FeedCreateResponse:
      type: object
      required:
      - data
      properties:
        data:
          $ref: "#/components/schemas/PresentFeed"
    FeedSyncResponse:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - jobId
          properties:
            jobId:
              type: string
              format: uuid
            job_id:
              type: string
              format: uuid
              description: Dual-support snake_case alias
    PresentFeed:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        url:
          type: string
          nullable: true
        item_path:
          type: string
        is_active:
          type: boolean
        product_count:
          type: integer
        status:
          type: string
        last_synced:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        feed_type:
          type: string
          description: Dual-support v2 field
        sync_interval_minutes:
          type: integer
          description: Dual-support v2 field
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        options:
          type: object
          additionalProperties: true
    FeedMappings:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Absent when no mappings row exists yet
        version:
          type: integer
        mappings:
          description: Field mapping document (object or array). Empty array when none saved.
          oneOf:
          - type: object
            additionalProperties: true
          - type: array
            items:
              type: object
              additionalProperties: true
    SchemaExtractResult:
      type: object
      required:
      - feed_id
      - format
      - fields
      - sample_rows
      properties:
        feed_id:
          type: string
          format: uuid
        format:
          type: string
          enum:
          - xml
          - csv
        suggested_item_path:
          type: string
        item_path:
          type: string
        fields:
          type: array
          items:
            type: object
            properties:
              path:
                type: string
              field_name:
                type: string
              data_type:
                type: string
              sample_values:
                type: array
                items:
                  type: string
              unique_values_count:
                type: integer
              suggested_target:
                type: string
        sample_rows:
          type: integer
        preview:
          type: string
        preview_truncated:
          type: boolean
    ExportFeedDetail:
      type: object
      description: Flat export_feeds row from GET/PATCH/PUT template handlers (not presentV1ExportFeed
        / data envelope).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        source_feed_id:
          type: string
          format: uuid
          nullable: true
        format:
          type: string
          enum:
          - xml
          - csv
        public_token:
          type: string
        template:
          type: object
          additionalProperties: true
          nullable: true
        filters:
          type: object
          additionalProperties: true
          nullable: true
        is_active:
          type: boolean
        last_generated_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    StatusOK:
      type: object
      required:
      - status
      properties:
        status:
          type: string
          example: ok
    FeedDeleted:
      type: object
      required:
      - id
      - deleted
      properties:
        id:
          type: string
          format: uuid
        deleted:
          type: boolean
    PreparedCampaign:
      type: object
      properties:
        preset_id:
          type: string
          enum:
          - black_friday
          - christmas
        name:
          type: string
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
        year:
          type: integer
        export_feed_id:
          type: string
          format: uuid
        export_feed_name:
          type: string
        created:
          type: boolean
          description: True only on newly prepared campaigns
    PreparedCampaignEnvelope:
      type: object
      required:
      - data
      properties:
        data:
          $ref: "#/components/schemas/PreparedCampaign"
    MarketingCalendar:
      type: object
      required:
      - year
      - presets
      - prepared
      properties:
        year:
          type: integer
        presets:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              description:
                type: string
              start_date:
                type: string
                format: date
              end_date:
                type: string
                format: date
              year:
                type: integer
        prepared:
          type: array
          items:
            $ref: "#/components/schemas/PreparedCampaign"
    ProcessingJob:
      type: object
      properties:
        id:
          type: string
          format: uuid
        company_id:
          type: string
          format: uuid
        status:
          type: string
          example: pending
        total_products:
          type: integer
        processed_products:
          type: integer
        processing_type:
          type: string
        current_step:
          type: string
        step_progress:
          type: array
          items:
            type: object
            properties:
              step:
                type: string
              status:
                type: string
              note:
                type: string
        error:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
            type: string
            format: date-time
        items:
            type: array
            description: Present when status is completed — processed product payload (additive)
            items:
            $ref: "#/components/schemas/LegacyProcessItem"
        total_items:
            type: integer
            description: Present with items when status is completed
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/ProcessingJob"
          description: Present when StartJob auto-splits into multiple jobs
        sibling_job_ids:
          type: array
          items:
            type: string
            format: uuid
        job_count:
          type: integer
        total_products_queued:
          type: integer
    ProcessingJobStartResponse:
      description: Single Job object, or Job plus split metadata (jobs, sibling_job_ids, …)
      allOf:
      - $ref: "#/components/schemas/ProcessingJob"
    PlanGateError:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
          enum:
          - insufficient_credits
          - product_limit
          - ai_requires_upgrade
          - eprel_requires_upgrade
          - plan_gate
        upgrade_url:
          type: string
          example: /pricing
    FlatAPIError:
      type: object
      required:
      - error
      properties:
        error:
          type: string
    LegacyAPIError:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              example: validation_error
            message:
              type: string
        requestId:
          type: string
    LegacyProcessStartEnvelope:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - process_id
          - message
          properties:
            process_id:
              type: string
              format: uuid
            message:
              type: string
            total_items:
              type: integer
            processed_items:
              type: integer
            job_count:
              type: integer
              description: Present when StartJob auto-splits
            sibling_job_ids:
              type: array
              items:
                type: string
                format: uuid
            total_products_queued:
              type: integer
            errors:
              type: array
              items:
                type: string
              description: Per-item upsert failures when some EANs still queued
    LegacyProcessStatusEnvelope:
      type: object
      required:
      - data
      properties:
        data:
          type: object
          required:
          - status
          - process_id
          properties:
            status:
              type: string
              description: Uppercase job status (COMPLETED, FAILED, PENDING, PROCESSING, …)
              example: COMPLETED
            process_id:
              type: string
              format: uuid
            processing_type:
              description: Echo of requested type (string or step array)
              oneOf:
              - type: string
              - type: array
                items:
                  type: string
            items:
              type: array
              description: Present when status is COMPLETED
              items:
                $ref: "#/components/schemas/LegacyProcessItem"
            total_items:
              type: integer
            processed_at:
              type: string
              format: date-time
            message:
              type: string
            error:
              type: string
              description: Present when status is FAILED
            created_at:
              type: string
              format: date-time
            started_at:
              type: string
              format: date-time
              nullable: true
    LegacyProcessItem:
        type: object
        required:
        - ean
        properties:
        ean:
            type: string
        id:
            type: string
            format: uuid
            description: |
              Legacy field: processed_products.id when enrichment succeeded.
              Do not treat as raw_products.id. Same value as processed_product_id.
        processed_product_id:
            type: string
            format: uuid
            description: |
              Explicit alias of id (processed_products.id). Prefer this name in new
              dual-mode clients; id remains for backward compatibility.
        raw_product_id:
            type: string
            format: uuid
            description: |
              raw_products.id for this job line. Use with POST /process raw_product_ids
              or dashboard catalog APIs. Present whenever the job product row exists.
        category:
            type: string
            nullable: true
        category_name:
            type: string
            nullable: true
        title:
            type: string
            nullable: true
        meta_title:
            type: string
            nullable: true
        meta_description:
            type: string
            nullable: true
        description:
            nullable: true
            oneOf:
            - type: string
            - type: array
            items:
                type: string
        attributes:
            type: object
            additionalProperties: true
            nullable: true
        main_image:
            type: string
            nullable: true
        more_images:
            type: array
            items:
                type: string
            nullable: true
        eprel:
            nullable: true
            type: object
            properties:
            label:
                type: string
            pdf:
                type: string
            energy_class:
                type: string
            energy_scale:
                type: string
        status:
            type: string
            description: Per-item outcome — processed on success; not_found / failed / cancelled otherwise
            example: processed
        error:
            type: string
  examples:
    ProcessingJobAccepted:
      summary: Single processing job accepted (POST /process)
      value:
        id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
        company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        status: pending
        total_products: 2
        processed_products: 0
        processing_type: full
        current_step: category
        step_progress:
        - step: category
          status: pending
        - step: title
          status: pending
        - step: description
          status: pending
        - step: attributes
          status: pending
        created_at: '2026-08-04T09:59:50Z'
    ProcessingJobSplitAccepted:
      summary: Auto-split batch (POST /process)
      value:
        id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
        company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        status: pending
        total_products: 50
        processed_products: 0
        processing_type: full
        current_step: category
        created_at: '2026-08-04T09:59:50Z'
        jobs:
        - id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
          status: pending
          total_products: 50
        - id: 1b4e28ba-2fa1-4d3a-9c6e-7f8a9b0c1d2e
          status: pending
          total_products: 50
        sibling_job_ids:
        - 1b4e28ba-2fa1-4d3a-9c6e-7f8a9b0c1d2e
        job_count: 2
        total_products_queued: 100
    ProcessingJobRunning:
      summary: "Processing job in progress (GET /process/{id})"
      value:
        id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
        company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        status: processing
        total_products: 25
        processed_products: 8
        processing_type: full
        current_step: title
        step_progress:
        - step: category
          status: done
        - step: title
          status: running
        - step: description
          status: pending
        - step: attributes
          status: pending
        started_at: '2026-08-04T10:00:00Z'
        created_at: '2026-08-04T09:59:50Z'
    LegacyProcessCompleted:
      summary: "Completed legacy poll (GET /products/process/{id})"
      value:
        data:
          status: COMPLETED
          process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
          processing_type: full
          items:
          - ean: '4548736132174'
            id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
            processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
            raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc
            status: processed
            category: electronics
            category_name: Electronics
            title: Sony WH-1000XM5 Wireless Noise Cancelling Headphones Black
            meta_title: Sony WH-1000XM5 | Noise Cancelling Headphones
            meta_description: Industry-leading noise cancellation with up to 30 hours battery life.
            description:
            - Industry-leading noise cancellation with up to 30 hours battery life.
            attributes:
              color: Black
              brand: Sony
              battery_life_hours: '30'
            main_image: https://images.example.com/products/wh1000xm5-black.jpg
            more_images:
            - https://images.example.com/products/wh1000xm5-black-side.jpg
            eprel: null
          total_items: 1
          processed_at: '2026-08-04T10:04:12Z'
    LegacyProcessInProgress:
      summary: In-progress legacy poll
      value:
        data:
          status: PROCESSING
          process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
          processing_type: full
          created_at: '2026-08-04T09:59:50Z'
          started_at: '2026-08-04T10:00:00Z'
    LegacyProcessFailed:
      summary: Failed legacy poll
      value:
        data:
          status: FAILED
          process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21
          processing_type: full
          error: Processing failed
  responses:
    Unauthorized:
      description: Missing or invalid API key (RequireAPIKey / CodedError legacy envelope).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/LegacyAPIError"
          example:
            error:
              code: unauthorized
              message: Unauthorized
    Forbidden:
      description: Wrong company or insufficient role. Flat error string. Cross-tenant resource ids on
        API-key routes usually return 404 instead.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/FlatAPIError"
          example:
            error: forbidden
    NotFound:
      description: Resource not found for this API key company (missing id or wrong tenant).
      content:
        application/json:
          schema:
            oneOf:
            - $ref: "#/components/schemas/FlatAPIError"
            - $ref: "#/components/schemas/LegacyAPIError"
          examples:
            flat:
              summary: Dashboard-shared handlers
              value:
                error: not found
            legacy:
              summary: Legacy v1Err helpers
              value:
                error:
                  code: not_found
                  message: Not found
    BadRequest:
      description: Invalid request or client validation error (handlers use HTTP 400).
      content:
        application/json:
          schema:
            oneOf:
            - $ref: "#/components/schemas/FlatAPIError"
            - $ref: "#/components/schemas/LegacyAPIError"
          examples:
            flat:
              summary: Flat Error helper
              value:
                error: invalid json
            legacy:
              summary: Legacy v1Err validation
              value:
                error:
                  code: validation_error
                  message: '''items'' array is required'
    ValidationError:
      description: Validation error. Public v1 handlers return HTTP 400 for these cases (OpenAPI also
        lists 422 for clients that expect that status).
      content:
        application/json:
          schema:
            oneOf:
            - $ref: "#/components/schemas/FlatAPIError"
            - $ref: "#/components/schemas/LegacyAPIError"
          examples:
            flat:
              summary: Flat validation
              value:
                error: invalid id
            legacy:
              summary: Legacy coded validation
              value:
                error:
                  code: validation_error
                  message: invalid id
    Conflict:
      description: Conflict (for example cannot demote or remove the last company admin).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/FlatAPIError"
          example:
            error: cannot demote the last admin
    TooManyRequests:
      description: "Rate limited (RateLimitV1Process heavy mutations and/or processing StartLimiter).\
        \ May include Retry-After: 60."
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until retry (set by RateLimitV1Process)
      content:
        application/json:
          schema:
            oneOf:
            - $ref: "#/components/schemas/FlatAPIError"
            - $ref: "#/components/schemas/LegacyAPIError"
          examples:
            flat:
              summary: Middleware limiter
              value:
                error: rate limit exceeded
            legacy:
              summary: Processing StartLimiter
              value:
                error:
                  code: rate_limited
                  message: rate limit exceeded
    InternalServerError:
      description: Unexpected server failure (auth backend, enqueue, list/get failures).
      content:
        application/json:
          schema:
            oneOf:
            - $ref: "#/components/schemas/FlatAPIError"
            - $ref: "#/components/schemas/LegacyAPIError"
          examples:
            flat:
              summary: Flat Error helper
              value:
                error: list failed
            legacy:
              summary: Legacy coded internal error
              value:
                error:
                  code: internal_error
                  message: list failed
