openapi: 3.0.0
info:
  title: Humind Public API
  description: >-
    Push and manage your catalog through Humind's public REST API. Authenticated with an API key generated from the
    dashboard at Settings → Developer → Credentials.
  version: 1.0.0
servers:
  - url: https://api.thehumind.com
    description: Production
tags:
  - name: Products (Public API)
    description: |
      Push and manage products through the public catalog API. Authenticated
      with an API key (`apiKeyAuth`). Idempotency-Key header recommended on
      all writes (will be required from v1.1).
  - name: Collections (Public API)
    description: |
      Push and manage product collections through the public catalog API.
      Collections group products and can carry per-language translations.
      Required scopes: `catalog:read` for GETs, `catalog:write` for writes.
  - name: Knowledge (Public API)
    description: |
      Push and manage knowledge items (snippets, articles, webpages) consumed
      by the AI chat agent. `file` (PDF) ships in a later phase. Required
      scopes: `knowledge:read` for GETs, `knowledge:write` for writes.
  - name: Imports (Public API)
    description: |
      Async NDJSON import pipeline for large catalogs (10k–100k+ products).
      Workflow: create an import → upload NDJSON to the returned SAS URL →
      start the import → poll status. Authenticated with an API key holding
      the `imports:write` scope.
  - name: Webhooks (Public API)
    description: |
      Outgoing webhooks. Register an HTTPS endpoint to receive event
      notifications such as `import.completed` and `import.failed`. Each
      delivery is signed with HMAC SHA256 (header `X-Humind-Signature:
      t=<unix-ts>,v1=<hex-digest>`), retried up to 7 times with
      exponential backoff [10s, 1m, 5m, 30m, 2h, 12h, 24h], and recorded
      under `/public/v1/webhooks/deliveries` for inspection. Authenticated
      with an API key holding the `webhooks:manage` scope.
security:
  - apiKeyAuth: []
paths:
  /public/v1/products:
    post:
      tags:
        - Products (Public API)
      summary: Create or update a product
      description: |
        Upsert a product by `external_id` within the authenticated company.
        Returns 201 if the product is freshly created, 200 if an existing one
        was updated. Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response (with `Idempotent-Replayed: true`).
            Reusing the key with a different body returns 409
            `idempotency_conflict`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicProduct'
      responses:
        '200':
          description: Existing product updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '201':
          description: Product created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: |
            Validation failed. Possible codes: `validation_failed`,
            `idempotency_key_required`, `idempotency_key_too_long`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: >-
            Authentication failed (`missing_credentials`, `invalid_key_format`, `invalid_checksum`, `invalid_key`,
            `revoked`, `expired`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` — the same Idempotency-Key was reused with
            a different body. `idempotency_in_progress` — another request
            with the same key is still in flight.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    get:
      tags:
        - Products (Public API)
      summary: List products
      description: |
        Returns up to `limit` products, scoped to the authenticated company and
        environment (live keys see only live products, test keys only test
        products). Required scope: `catalog:read`. Pagination is cursor-based:
        pass the `next_cursor` from the previous response as `cursor` to
        retrieve the next page. Items are ordered newest-first by id.
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: handle
          required: false
          schema:
            type: string
          description: Filter by exact product handle.
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum:
              - active
              - draft
              - archived
          description: Filter by status.
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Max items to return.
        - in: query
          name: cursor
          required: false
          schema:
            type: string
          description: |
            Opaque pagination token returned as `next_cursor` from a previous
            page. Treat it as opaque — its format may change without notice.
      responses:
        '200':
          description: List of products
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PublicProduct'
                  next_cursor:
                    type: string
                    nullable: true
                    description: |
                      Pass this back as the `cursor` query parameter to fetch
                      the next page. `null` indicates the end of the result set.
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/products/batch:
    post:
      tags:
        - Products (Public API)
      summary: Batch create or update products
      description: |
        Upserts up to 500 products in a single request. Each item is processed
        independently — a failure on one item does not abort the batch.
        Returns 207 Multi-Status with per-item results. Required scope:
        `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicProductBatchRequest'
      responses:
        '207':
          description: Per-item results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProductBatchResponse'
        '400':
          description: |
            Batch payload is empty, exceeds 500 items, is not a JSON array, or
            the `Idempotency-Key` header is missing / too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/products/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
        description: |
          Either `api:<external_id>` (the merchant's own id, prefixed) or
          `<humind_id>` (24-char ObjectId hex).
    get:
      tags:
        - Products (Public API)
      summary: Retrieve a product by id
      description: Required scope `catalog:read`. Cross-tenant ids resolve to 404.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found in the authenticated company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    put:
      tags:
        - Products (Public API)
      summary: Replace a product (full)
      description: |
        Full replace by id. The product's `external_id` is locked to the doc
        on disk, so a PUT by `humind_id` cannot rebrand the product.
        Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicProduct'
      responses:
        '200':
          description: Product replaced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: |
            Invalid id or payload, or the `Idempotency-Key` header is missing /
            too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found in the authenticated company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    patch:
      tags:
        - Products (Public API)
      summary: Update a product (partial)
      description: |
        Partial update. Fields omitted from the body are left unchanged. The
        merged payload must still satisfy the full PublicProduct contract.
        Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicProductPatch'
      responses:
        '200':
          description: Product updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: |
            Invalid id or payload, or the `Idempotency-Key` header is missing /
            too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found in the authenticated company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Products (Public API)
      summary: Archive a product (soft delete)
      description: |
        Soft delete: sets `status` to `archived`. We never hard-delete via the
        public API because downstream caches (chat agent context, vector
        index) would miss the deletion. Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Product archived
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found in the authenticated company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/collections:
    post:
      tags:
        - Collections (Public API)
      summary: Create or update a collection
      description: |
        Upsert a collection by `external_id` within the authenticated company.
        Returns 201 if freshly created, 200 if an existing one was updated.
        Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
          description: Recommended on writes. Will be required from v1.1.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCollection'
      responses:
        '200':
          description: Existing collection updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '201':
          description: Collection created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    get:
      tags:
        - Collections (Public API)
      summary: List collections
      description: |
        Returns up to `limit` collections, scoped to the authenticated company
        and environment. Required scope: `catalog:read`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: handle
          required: false
          schema:
            type: string
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum:
              - active
              - archived
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: List of collections
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PublicCollection'
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/collections/batch:
    post:
      tags:
        - Collections (Public API)
      summary: Batch create or update collections
      description: |
        Upserts up to 500 collections in a single request. Each item is
        processed independently — a failure on one item does not abort the
        batch. Returns 207 Multi-Status with per-item results. Required
        scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCollectionBatchRequest'
      responses:
        '207':
          description: Per-item results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollectionBatchResponse'
        '400':
          description: Batch payload is empty, exceeds 500 items, or is not a JSON array
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/collections/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
        description: Either `api:<external_id>` or `<humind_id>` (24-char hex).
    get:
      tags:
        - Collections (Public API)
      summary: Retrieve a collection by id
      description: Required scope `catalog:read`. Cross-tenant ids resolve to 404.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Collection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    put:
      tags:
        - Collections (Public API)
      summary: Replace a collection (full)
      description: |
        Full replace by id. The `external_id` is locked to the doc on disk.
        Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCollection'
      responses:
        '200':
          description: Collection replaced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '400':
          description: Invalid id or payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    patch:
      tags:
        - Collections (Public API)
      summary: Update a collection (partial)
      description: |
        Partial update. Fields omitted from the body are left unchanged. The
        merged payload must satisfy the full PublicCollection contract.
        Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCollectionPatch'
      responses:
        '200':
          description: Collection updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '400':
          description: Invalid id or payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Collections (Public API)
      summary: Archive a collection (soft delete)
      description: |
        Soft delete — the doc is kept and `status` is set to `archived` so
        downstream consumers see the change. Required scope: `catalog:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Collection archived
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge:
    post:
      tags:
        - Knowledge (Public API)
      summary: Create or update a knowledge item
      description: |
        Upsert a knowledge item by `external_id` within the authenticated
        company. Returns 201 if freshly created, 200 if an existing one was
        updated. Required scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicKnowledge'
      responses:
        '200':
          description: Existing item updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '201':
          description: Item created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: Validation failed (also 400 for unsupported `type=file`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: Type lock — incoming `type` does not match the existing item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    get:
      tags:
        - Knowledge (Public API)
      summary: List knowledge items
      description: Required scope `knowledge:read`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: type
          required: false
          schema:
            type: string
            enum:
              - snippet
              - article
              - webpage
              - file
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum:
              - draft
              - published
              - archived
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: List of knowledge items
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PublicKnowledge'
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge/batch:
    post:
      tags:
        - Knowledge (Public API)
      summary: Batch create or update knowledge items
      description: |
        Upserts up to 500 knowledge items in a single request. Each item is
        processed independently. Returns 207 Multi-Status with per-item
        results. Required scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicKnowledgeBatchRequest'
      responses:
        '207':
          description: Per-item results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledgeBatchResponse'
        '400':
          description: Batch payload is empty, exceeds 500 items, or is not a JSON array
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
        description: Either `api:<external_id>` or `<humind_id>` (24-char hex).
    get:
      tags:
        - Knowledge (Public API)
      summary: Retrieve a knowledge item by id
      description: Required scope `knowledge:read`.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Knowledge item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    put:
      tags:
        - Knowledge (Public API)
      summary: Replace a knowledge item (full)
      description: |
        Full replace by id. `type` and `external_id` are both locked to the
        existing doc — a PUT cannot change the discriminator. Required
        scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicKnowledge'
      responses:
        '200':
          description: Item replaced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: Invalid id or payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    patch:
      tags:
        - Knowledge (Public API)
      summary: Update a knowledge item (partial)
      description: |
        Partial update. `type` and `external_id` are locked. Required
        scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicKnowledgePatch'
      responses:
        '200':
          description: Item updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: Invalid id or payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Knowledge (Public API)
      summary: Archive a knowledge item (soft delete)
      description: |
        Soft delete — sets `status` to `archived`. Required scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Item archived
        '400':
          description: Invalid id format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/imports:
    post:
      tags:
        - Imports (Public API)
      summary: Create an async import
      description: |
        Creates an import record and returns a pre-signed Azure Blob SAS URL.
        Upload your NDJSON catalog (one PublicProduct per line) to that URL,
        then call `POST /public/v1/imports/{id}/start`. Required scope:
        `imports:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicImportCreateRequest'
      responses:
        '201':
          description: Import created. Upload your NDJSON to `upload_url` before starting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicImportCreateResponse'
        '400':
          description: |
            Invalid `resource_type` or `format`, or the `Idempotency-Key`
            header is missing / too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/imports/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: The `sync_id` returned by `POST /public/v1/imports`.
    get:
      tags:
        - Imports (Public API)
      summary: Get import status
      description: |
        Returns the current status, progress counters, and the last 100 error
        log entries. Required scope: `imports:write` (no separate read scope).
        Cross-tenant ids resolve to 404.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Import status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicImportStatus'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Import not found in the authenticated company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/imports/{id}/start:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Imports (Public API)
      summary: Start processing an uploaded import
      description: |
        Verifies the NDJSON upload exists at the SAS URL and schedules the
        async processor. Returns 202 immediately — poll status to track
        progress. Required scope: `imports:write`.
      security:
        - apiKeyAuth: []
      responses:
        '202':
          description: Import accepted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicImportStartResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Import not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            Import is not in `pending` status, or the NDJSON upload has not
            been received at the SAS URL yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/imports/{id}/cancel:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Imports (Public API)
      summary: Cancel an in-flight import
      description: |
        Marks the import as `cancelled`. The processor checks the DB every 50
        rows and aborts on the next checkpoint. Already-upserted products are
        not rolled back. Required scope: `imports:write`.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Import cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicImportCancelResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Import not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: Import is in a terminal status (`done`, `failed`, `cancelled`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/webhooks/endpoints:
    post:
      tags:
        - Webhooks (Public API)
      summary: Register a webhook endpoint
      description: |
        Registers a new webhook endpoint and returns a cleartext signing
        secret (`whsec_*`). The secret is shown **once** — store it
        immediately. Required scope: `webhooks:manage`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointCreateRequest'
      responses:
        '201':
          description: Endpoint created. Capture `secret` — it is not retrievable later.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointCreateResponse'
        '400':
          description: |
            Invalid payload (URL or events), or the `Idempotency-Key` header
            is missing / too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    get:
      tags:
        - Webhooks (Public API)
      summary: List webhook endpoints
      description: Lists all webhook endpoints owned by the authenticated company. Required scope `webhooks:manage`.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Endpoints owned by this company
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/webhooks/endpoints/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
        description: Webhook endpoint id.
    get:
      tags:
        - Webhooks (Public API)
      summary: Get a webhook endpoint
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Endpoint not found in this company
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    patch:
      tags:
        - Webhooks (Public API)
      summary: Update a webhook endpoint
      description: |
        Update url, subscribed events, or status. At least one of those
        three must be present. Re-enabling a previously auto-disabled
        endpoint resets its `failure_count` to 0.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointPatch'
      responses:
        '200':
          description: Updated endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '400':
          description: |
            Invalid patch, or the `Idempotency-Key` header is missing /
            too long.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Webhooks (Public API)
      summary: Delete a webhook endpoint
      description: |
        Hard delete. Pending deliveries for this endpoint are removed at
        the same time; already delivered / failed deliveries stay until
        their 30-day TTL expires.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Deleted
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/webhooks/endpoints/{id}/rotate-secret:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
    post:
      tags:
        - Webhooks (Public API)
      summary: Rotate the signing secret
      description: |
        Generates a new cleartext secret and immediately invalidates the
        previous one. There is no grace period — coordinate the swap on
        your side. Required scope: `webhooks:manage`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
          description: |
            **Required.** Replays of the same key within 24h with an identical
            body return the cached response.
      responses:
        '200':
          description: New secret generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointRotateResponse'
        '400':
          description: |
            `idempotency_key_required` / `idempotency_key_too_long`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: |
            `idempotency_conflict` or `idempotency_in_progress`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/webhooks/deliveries:
    get:
      tags:
        - Webhooks (Public API)
      summary: List recent deliveries
      description: |
        Returns up to 200 most-recent deliveries for the authenticated
        company. Optional filters: `endpoint_id`, `status`. The default
        and maximum limits are 100 and 200 respectively.
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: endpoint_id
          schema:
            type: string
          description: Restrict to one endpoint.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - pending
              - delivered
              - failed
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 100
      responses:
        '200':
          description: Deliveries (most recent first)
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
        '400':
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/webhooks/deliveries/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
    get:
      tags:
        - Webhooks (Public API)
      summary: Get a delivery
      description: |
        Returns the full delivery record including the JSON payload sent
        to your endpoint and every attempt's response status / excerpt.
        Useful for debugging signature verification on your side.
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDelivery'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Delivery not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/products/{id}/translations/{lang}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
      - in: path
        name: lang
        required: true
        schema:
          type: string
          pattern: ^[a-z]{2}(-[A-Z]{2})?$
        description: BCP 47 language code (e.g. `en`, `fr`, `pt-BR`).
    put:
      tags:
        - Products (Public API)
      summary: Set or update a product translation for one language
      description: Required scope `catalog:write`. Body fields are all optional — supply only the fields you want to set or change.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicProductTranslation'
      responses:
        '200':
          description: Updated product (with the new translation merged in)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Products (Public API)
      summary: Remove a product translation
      description: Idempotent — removing a missing language returns 204. Required scope `catalog:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Translation removed (or already absent)
        '400':
          description: Invalid id or lang
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/products/{id}/variants/{variant_external_id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
      - in: path
        name: variant_external_id
        required: true
        schema:
          type: string
    put:
      tags:
        - Products (Public API)
      summary: Set or update a single variant
      description: |
        Find-or-push the variant by `variant_external_id` (path locked — body
        cannot rebrand it). Returns the full Product with the variant change
        merged in. Required scope `catalog:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicVariant'
      responses:
        '200':
          description: Updated product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProduct'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Products (Public API)
      summary: Remove a variant
      description: |
        Refuses to remove the LAST variant — Product schema requires at least
        one. Returns 422 `cannot_delete_last_variant` in that case. Otherwise
        idempotent (missing variant → 204). Required scope `catalog:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Variant removed (or already absent)
        '400':
          description: Invalid id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Product not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '422':
          description: cannot_delete_last_variant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/collections/{id}/translations/{lang}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
      - in: path
        name: lang
        required: true
        schema:
          type: string
          pattern: ^[a-z]{2}(-[A-Z]{2})?$
    put:
      tags:
        - Collections (Public API)
      summary: Set or update a collection translation
      description: Required scope `catalog:write`. Body fields all optional.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCollectionTranslation'
      responses:
        '200':
          description: Updated collection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCollection'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Collections (Public API)
      summary: Remove a collection translation
      description: Idempotent. Required scope `catalog:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Translation removed (or already absent)
        '400':
          description: Invalid id or lang
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Collection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge/{id}/translations/{lang}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
      - in: path
        name: lang
        required: true
        schema:
          type: string
          pattern: ^[a-z]{2}(-[A-Z]{2})?$
    put:
      tags:
        - Knowledge (Public API)
      summary: Set or update a knowledge translation
      description: |
        Body shape depends on the existing item's `type`. For `snippet`: supply
        `title?` / `content?`. For `article` / `webpage`: supply `title?` /
        `content_html?`. Required scope `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                title:
                  type: string
                  minLength: 1
                content:
                  type: string
                  description: Required field for snippet type.
                content_html:
                  type: string
                  description: Required field for article/webpage type.
      responses:
        '200':
          description: Updated knowledge item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: Validation failed (type/body mismatch)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Knowledge item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
    delete:
      tags:
        - Knowledge (Public API)
      summary: Remove a knowledge translation
      description: Idempotent. Required scope `knowledge:write`.
      security:
        - apiKeyAuth: []
      responses:
        '204':
          description: Translation removed (or already absent)
        '400':
          description: Invalid id or lang
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Knowledge item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge/{id}/file-upload-url:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
    post:
      tags:
        - Knowledge (Public API)
      summary: Mint a SAS URL to upload a knowledge file
      description: |
        Available only on knowledge items with `type='file'`. Returns a
        write-only Azure Blob SAS URL — `PUT` the file payload (PDF, DOCX,
        TXT, MD, HTML, RTF) directly to `upload_url`, then call
        `POST /knowledge/{id}/file-upload-completed` to finalise.

        The SAS URL expires after 1 hour. Calling this endpoint again on the
        same item mints a fresh URL with a new expiry.

        Required scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      responses:
        '200':
          description: Upload URL minted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledgeFileUploadUrlResponse'
        '400':
          description: |
            `validation_failed` (invalid id) or `wrong_type` (the item exists
            but isn't a `type='file'` knowledge).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Knowledge item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
  /public/v1/knowledge/{id}/file-upload-completed:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^(api:.+|[a-f0-9]{24})$
    post:
      tags:
        - Knowledge (Public API)
      summary: Finalise a knowledge file upload
      description: |
        Call this once you've successfully `PUT` your file to the
        `upload_url` returned by `POST /knowledge/{id}/file-upload-url`. The
        server verifies the blob exists, records `file_size` / `file_url` /
        `uploaded_at`, and flips `status` to `published` so the chat agent
        can start grounding answers on it.

        Returns 422 `file_not_uploaded` if no blob is present yet — `PUT`
        the file first, then retry.

        Idempotent: calling it twice after a successful upload re-publishes
        with a fresh `uploaded_at`.

        Required scope: `knowledge:write`.
      security:
        - apiKeyAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            maxLength: 255
      responses:
        '200':
          description: File finalised — full knowledge item returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKnowledge'
        '400':
          description: |
            `validation_failed` (invalid id) or `wrong_type` (the item exists
            but isn't a `type='file'` knowledge).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '404':
          description: Knowledge item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '409':
          description: idempotency_conflict / idempotency_in_progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '422':
          description: |
            `file_not_uploaded` — no blob has been uploaded to the SAS URL
            yet. PUT the file before calling this endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          description: |
            Rate limited. `code: rate_limited` is per-API-key throttling
            (in-memory token bucket — Phase 3.5+ moves to Redis). `code:
            too_many_failures` covers the auth-layer IP throttle (5 failed
            auths/min/IP). The `Retry-After` header (seconds) and
            `details.retry_after` always agree.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Always present on 429.
              schema:
                type: integer
                minimum: 1
            X-RateLimit-Limit:
              description: Sustained per-minute cap for this endpoint group.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Tokens left in the bucket. 0 when the request was throttled.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch (seconds) at which the bucket fully refills.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
components:
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: hmd_(live|test)_<22 chars base62>_<8 chars CRC32 hex>
      description: |
        Public API authentication. Generate a key from the dashboard at
        Settings → Developer → Credentials. The key is shown once at creation
        and cannot be retrieved later. Required scopes are listed per endpoint.
  schemas:
    PublicAdditionalSection:
      type: object
      required:
        - key
        - title
        - content
      properties:
        key:
          type: string
          minLength: 1
        title:
          type: string
          minLength: 1
        content:
          type: string
          minLength: 1
    PublicBrand:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
        domain:
          type: string
    PublicCartAction:
      type: object
      required:
        - type
      description: |
        Public clients can only emit `redirect` (open the merchant's URL) or
        `noop` (no checkout). Platform-internal cart types (`shopify`,
        `woocommerce`) require ids the merchant doesn't have and are not
        accepted on this surface.
      properties:
        type:
          type: string
          enum:
            - redirect
            - noop
        url:
          type: string
          format: uri
    PublicCollection:
      type: object
      required:
        - external_id
        - title
      properties:
        external_id:
          type: string
          minLength: 1
          description: Merchant-supplied stable identifier. Used for upsert.
        humind_id:
          type: string
          readOnly: true
          description: Humind-internal collection id (24-char hex). Returned only.
        handle:
          type: string
          description: |
            URL-friendly handle, unique per company. Server derives one from the
            `external_id` when omitted on create.
        title:
          type: string
          minLength: 1
        description:
          type: string
        description_html:
          type: string
        status:
          type: string
          enum:
            - active
            - archived
          default: active
        default_language:
          type: string
          description: BCP-47 language code, e.g. `fr` or `en-US`.
        products:
          type: array
          items:
            $ref: '#/components/schemas/PublicCollectionProductRef'
        translations:
          type: array
          description: Per-language overrides. Existing language entries absent from a write are preserved.
          items:
            $ref: '#/components/schemas/PublicCollectionTranslation'
        unresolved_products:
          type: array
          readOnly: true
          description: |
            Returned on writes when one or more `products[].external_id`s could
            not be matched to an existing product in the company. The collection
            is still created/updated — unresolved refs are just dropped.
          items:
            type: string
    PublicCollectionBatchRequest:
      type: array
      minItems: 1
      maxItems: 500
      description: Batch upsert payload (1–500 collections). Items are processed independently.
      items:
        $ref: '#/components/schemas/PublicCollection'
    PublicCollectionBatchResponse:
      type: object
      required:
        - results
      properties:
        results:
          type: array
          items:
            type: object
            required:
              - external_id
              - status
            properties:
              external_id:
                type: string
                nullable: true
              status:
                type: string
                enum:
                  - created
                  - updated
                  - failed
              humind_id:
                type: string
              unresolved_products:
                type: array
                items:
                  type: string
              error:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  details:
                    type: object
                    additionalProperties: true
    PublicCollectionPatch:
      type: object
      description: |
        Partial collection. Any field omitted is left unchanged. `external_id`
        is locked on PATCH — the value on the existing document always wins.
      properties:
        handle:
          type: string
        title:
          type: string
          minLength: 1
        description:
          type: string
        description_html:
          type: string
        status:
          type: string
          enum:
            - active
            - archived
        default_language:
          type: string
        products:
          type: array
          items:
            $ref: '#/components/schemas/PublicCollectionProductRef'
        translations:
          type: array
          items:
            $ref: '#/components/schemas/PublicCollectionTranslation'
    PublicCollectionProductRef:
      type: object
      required:
        - external_id
      properties:
        external_id:
          type: string
          minLength: 1
          description: Merchant-supplied product `external_id` (the same one used in PublicProduct).
        humind_product_id:
          type: string
          readOnly: true
          description: |
            The Humind-internal product id this reference resolved to. Returned
            on read; ignored on write.
        include_all_variants:
          type: boolean
          default: true
          description: When true, every variant of the referenced product is in the collection.
        included_variants:
          type: array
          description: |
            When `include_all_variants` is false, the variant `external_id`s to
            include. The server resolves them to internal variant ids.
          items:
            type: string
    PublicCollectionTranslation:
      type: object
      required:
        - language
      properties:
        language:
          type: string
          description: BCP-47 language code, e.g. `en` or `fr-FR`.
        title:
          type: string
        description:
          type: string
        description_html:
          type: string
    PublicError:
      type: object
      required:
        - error
      description: |
        Uniform error envelope for the public catalog API. The `code` is
        machine-readable and stable; the `message` is for humans and may
        change. `request_id` echoes the server-side request id for support
        triage.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: |
                Stable machine-readable error code. One of:
                `missing_credentials`, `invalid_key_format`, `invalid_checksum`,
                `invalid_key`, `revoked`, `expired`, `insufficient_scope`,
                `too_many_failures`, `rate_limited`, `validation_failed`,
                `not_found`, `idempotency_conflict` (Phase 2),
                `upsert_failed`, `internal_error`.
              example: validation_failed
            message:
              type: string
              example: Invalid product payload.
            request_id:
              type: string
              description: Server-side request id; quote it when filing a support ticket.
            details:
              type: object
              additionalProperties: true
              description: Optional structured context (validation issues, missing scopes, etc.).
    PublicImage:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          minLength: 1
        alt:
          type: string
    PublicImportCancelResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - cancelled
    PublicImportCreateRequest:
      type: object
      required:
        - resource_type
        - format
      properties:
        resource_type:
          type: string
          enum:
            - product
          description: Only `product` is supported in Phase 3A.
        format:
          type: string
          enum:
            - ndjson
          description: Only `ndjson` (newline-delimited JSON) is supported.
    PublicImportCreateResponse:
      type: object
      required:
        - sync_id
        - upload_url
        - expires_at
        - status
      properties:
        sync_id:
          type: string
          format: uuid
          description: Stable id for the import. Use for status, start, cancel.
        upload_url:
          type: string
          format: uri
          description: |
            Pre-signed Azure Blob SAS URL with `Create + Write` permissions.
            PUT your NDJSON payload here (one PublicProduct per line).
            Expires after 1 hour.
        expires_at:
          type: string
          format: date-time
          description: When the SAS URL expires.
        status:
          type: string
          enum:
            - pending
    PublicImportStartResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - processing
    PublicImportStatus:
      type: object
      required:
        - sync_id
        - status
        - resource_type
        - total_products
        - synced_products
        - report
        - error_logs
      properties:
        sync_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - processing
            - done
            - failed
            - cancelled
        resource_type:
          type: string
          enum:
            - product
        total_products:
          type: integer
          minimum: 0
          description: Total rows seen in the NDJSON file (includes invalid rows).
        synced_products:
          type: integer
          minimum: 0
          description: Rows that produced a created or updated product (`report.created + report.updated`).
        report:
          type: object
          required:
            - created
            - updated
            - failed
          properties:
            created:
              type: integer
              minimum: 0
            updated:
              type: integer
              minimum: 0
            failed:
              type: integer
              minimum: 0
        error_logs:
          type: array
          description: >-
            Last 100 error entries from the import. Counts in `report.failed` are exact even when the error log is
            capped.
          items:
            type: object
            required:
              - message
              - timestamp
            properties:
              message:
                type: string
              product_id:
                type: string
                description: The merchant's `external_id` for the offending row, if available.
              timestamp:
                type: string
                format: date-time
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
    PublicKnowledge:
      type: object
      required:
        - external_id
        - type
        - title
      description: |
        Discriminated by `type`. Required additional fields:
          * `snippet` → `content`
          * `article` → `content_html`
          * `webpage` → `url`. Supplying `content_html` publishes the page
            immediately; omitting it queues a background fetch (status=queued).
          * `file` → `original_filename` + `mime_type`. Creates a placeholder
            doc (`status=draft`); the actual blob is uploaded out-of-band via
            `POST /knowledge/{id}/file-upload-url` and finalised by
            `POST /knowledge/{id}/file-upload-completed`.
      properties:
        external_id:
          type: string
          minLength: 1
          description: Merchant-supplied stable identifier. Used for upsert.
        humind_id:
          type: string
          readOnly: true
          description: Humind-internal knowledge id (24-char hex). Returned only.
        type:
          type: string
          enum:
            - snippet
            - article
            - webpage
            - file
          description: Discriminator. Locked once set — cannot change on update.
        title:
          type: string
          minLength: 1
        tags:
          type: array
          description: Tag names. The server `findOrCreate`s tags by lowercased name within the company.
          items:
            type: string
        is_available_for_ai_agent:
          type: boolean
          default: true
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          default: published
          description: |
            Internal transitional states (`queued`, `processing`, `failed`) are
            returned on read for `webpage` items but cannot be written directly.
            `file` items always start as `draft` and become `published` after a
            successful `POST /knowledge/{id}/file-upload-completed` call.
        active_from:
          type: string
          format: date-time
        active_until:
          type: string
          format: date-time
        content:
          type: string
          description: Required for `type=snippet`.
        content_html:
          type: string
          description: |
            Required for `type=article`. Optional for `type=webpage` — when
            omitted, the server schedules an async fetch.
        url:
          type: string
          format: uri
          description: Required for `type=webpage`. Must use https://.
        original_filename:
          type: string
          maxLength: 255
          description: |
            Required for `type=file`. The file's display name shown in the
            dashboard and used to build the storage blob path.
        mime_type:
          type: string
          enum:
            - application/pdf
            - application/vnd.openxmlformats-officedocument.wordprocessingml.document
            - application/msword
            - text/plain
            - text/markdown
            - text/html
            - application/rtf
          description: Required for `type=file`. Allowlist enforced server-side.
        file_size:
          type: integer
          nullable: true
          readOnly: true
          description: |
            File size in bytes. Populated by `POST /knowledge/{id}/file-upload-completed`.
            `null` while the upload is still pending.
        file_url:
          type: string
          nullable: true
          readOnly: true
          description: |
            Relative path of the file inside the knowledge-files Azure Blob
            container. `null` while the upload is still pending. Treat as opaque.
        uploaded_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
          description: When the file finished uploading. `null` until then.
        queued_for_fetch:
          type: boolean
          readOnly: true
          description: Returned on writes for `webpage` items that were queued for an async fetch.
    PublicKnowledgeBatchRequest:
      type: array
      minItems: 1
      maxItems: 500
      description: Batch upsert payload (1–500 knowledge items). Items are processed independently.
      items:
        $ref: '#/components/schemas/PublicKnowledge'
    PublicKnowledgeBatchResponse:
      type: object
      required:
        - results
      properties:
        results:
          type: array
          items:
            type: object
            required:
              - external_id
              - status
            properties:
              external_id:
                type: string
                nullable: true
              status:
                type: string
                enum:
                  - created
                  - updated
                  - failed
              humind_id:
                type: string
              queued_for_fetch:
                type: boolean
              error:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  details:
                    type: object
                    additionalProperties: true
    PublicKnowledgeFileUploadUrlResponse:
      type: object
      required:
        - upload_url
        - expires_at
        - blob_path
      description: |
        Response of `POST /knowledge/{id}/file-upload-url`. The merchant should
        PUT the file payload directly to `upload_url`. After a successful PUT,
        call `POST /knowledge/{id}/file-upload-completed` to finalise the doc.
      properties:
        upload_url:
          type: string
          format: uri
          description: |
            Pre-signed Azure Blob SAS URL with `Create + Write` permissions
            (no Read). Expires after 1 hour.
        expires_at:
          type: string
          format: date-time
          description: When the SAS URL stops working.
        blob_path:
          type: string
          description: |
            Opaque relative path. Returned for debugging / logging only — do
            not parse. The merchant only needs `upload_url`.
    PublicKnowledgePatch:
      type: object
      description: |
        Partial knowledge item. `type` and `external_id` are locked on PATCH.
        Type-specific fields validated against the existing item's `type`.
      properties:
        title:
          type: string
          minLength: 1
        tags:
          type: array
          items:
            type: string
        is_available_for_ai_agent:
          type: boolean
        status:
          type: string
          enum:
            - draft
            - published
            - archived
        active_from:
          type: string
          format: date-time
          nullable: true
        active_until:
          type: string
          format: date-time
          nullable: true
        content:
          type: string
        content_html:
          type: string
        url:
          type: string
          format: uri
    PublicProduct:
      type: object
      required:
        - external_id
        - title
        - variants
      properties:
        external_id:
          type: string
          minLength: 1
          description: Merchant-supplied stable identifier. Used for upsert.
        humind_id:
          type: string
          readOnly: true
          description: Humind-internal product id (24-char hex). Returned only.
        title:
          type: string
          minLength: 1
        description:
          type: string
        description_html:
          type: string
        handle:
          type: string
        type:
          type: string
          enum:
            - product
            - kit
          default: product
        status:
          type: string
          enum:
            - active
            - draft
            - archived
          default: active
        online_store_url:
          type: string
        default_language:
          type: string
          description: BCP-47 language code, e.g. `fr` or `en-US`.
          example: fr
        available_for_sale:
          type: boolean
          default: true
        brand:
          $ref: '#/components/schemas/PublicBrand'
        categories:
          type: array
          description: Category names; categories are matched/created by name within the company scope.
          items:
            type: string
        images:
          type: array
          items:
            $ref: '#/components/schemas/PublicImage'
        variants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/PublicVariant'
        translations:
          type: object
          description: Per-language overrides keyed by language code (e.g. `en`, `fr`, `es`).
          additionalProperties:
            $ref: '#/components/schemas/PublicProductTranslation'
        snippets:
          type: array
          items:
            $ref: '#/components/schemas/PublicSnippet'
        additional_sections:
          type: array
          items:
            $ref: '#/components/schemas/PublicAdditionalSection'
        ingredients:
          type: array
          items:
            type: string
    PublicProductBatchRequest:
      type: array
      minItems: 1
      maxItems: 500
      description: |
        Batch upsert payload. The body is a JSON array of `PublicProduct`
        objects (1–500 items). Items are processed independently — a failure
        on one item does not abort the rest.
      items:
        $ref: '#/components/schemas/PublicProduct'
    PublicProductBatchResponse:
      type: object
      required:
        - results
      properties:
        results:
          type: array
          items:
            type: object
            required:
              - external_id
              - status
            properties:
              external_id:
                type: string
                nullable: true
                description: Echo of the request item's `external_id` (null if the request item did not include one).
              status:
                type: string
                enum:
                  - created
                  - updated
                  - failed
              humind_id:
                type: string
                description: Humind-internal product id (set only on `created` or `updated`).
              error:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                    description: One of `validation_failed`, `upsert_failed`, etc.
                  message:
                    type: string
                  details:
                    type: object
                    additionalProperties: true
    PublicProductPatch:
      type: object
      description: |
        Partial product. Any field omitted is left unchanged. `external_id` is
        locked on PATCH — the value on the existing document always wins.
      properties:
        title:
          type: string
          minLength: 1
        description:
          type: string
        description_html:
          type: string
        handle:
          type: string
        type:
          type: string
          enum:
            - product
            - kit
        status:
          type: string
          enum:
            - active
            - draft
            - archived
        online_store_url:
          type: string
        default_language:
          type: string
        available_for_sale:
          type: boolean
        brand:
          $ref: '#/components/schemas/PublicBrand'
        categories:
          type: array
          items:
            type: string
        images:
          type: array
          items:
            $ref: '#/components/schemas/PublicImage'
        variants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/PublicVariant'
        translations:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/PublicProductTranslation'
        snippets:
          type: array
          items:
            $ref: '#/components/schemas/PublicSnippet'
        additional_sections:
          type: array
          items:
            $ref: '#/components/schemas/PublicAdditionalSection'
        ingredients:
          type: array
          items:
            type: string
    PublicProductTranslation:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        description_html:
          type: string
        handle:
          type: string
        online_store_url:
          type: string
        ingredients:
          type: array
          items:
            type: string
    PublicRegionalPrice:
      type: object
      required:
        - currency
        - price
      properties:
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217 currency code (uppercased server-side).
          example: EUR
        price:
          type: number
          minimum: 0
        compare_at_price:
          type: number
          minimum: 0
    PublicSnippet:
      type: object
      required:
        - title
        - content
      properties:
        title:
          type: string
          minLength: 1
        content:
          type: string
          minLength: 1
    PublicVariant:
      type: object
      required:
        - external_id
        - price
        - currency
        - cart_action
      properties:
        external_id:
          type: string
          minLength: 1
          description: Merchant-supplied stable identifier for the variant.
        title:
          type: string
        sku:
          type: string
        price:
          type: number
          minimum: 0
        compare_at_price:
          type: number
          minimum: 0
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217 currency code (uppercased server-side).
          example: EUR
        available_for_sale:
          type: boolean
          default: true
        inventory_quantity:
          type: integer
          minimum: 0
        image_url:
          type: string
        regional_pricing:
          type: object
          description: |
            Per-country overrides keyed by ISO 3166-1 alpha-2 country code
            (uppercased server-side). Each value is a `PublicRegionalPrice`.
          additionalProperties:
            $ref: '#/components/schemas/PublicRegionalPrice'
          example:
            FR:
              currency: EUR
              price: 19.9
            US:
              currency: USD
              price: 21
              compare_at_price: 25
        cart_action:
          $ref: '#/components/schemas/PublicCartAction'
    WebhookDelivery:
      type: object
      required:
        - id
        - webhook_endpoint_id
        - event_type
        - event_id
        - status
        - payload
        - attempts
        - created_at
      properties:
        id:
          type: string
        webhook_endpoint_id:
          type: string
        event_type:
          $ref: '#/components/schemas/WebhookEventType'
        event_id:
          type: string
          format: uuid
          description: |
            UUID echoed in the `X-Humind-Event-Id` header. Use it as an
            idempotency key on your side: retries of the same delivery
            (up to 7 attempts) all carry the same `event_id`.
        status:
          type: string
          enum:
            - pending
            - delivered
            - failed
        payload:
          type: object
          description: Snapshot of the JSON body POSTed to your endpoint.
        attempts:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliveryAttempt'
        next_attempt_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        permanently_failed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    WebhookDeliveryAttempt:
      type: object
      required:
        - at
      properties:
        at:
          type: string
          format: date-time
        response_status:
          type: integer
          nullable: true
        response_body_excerpt:
          type: string
          nullable: true
          description: First 500 chars of the merchant response body, when one was received.
        error:
          type: string
          nullable: true
          description: Local error label when no HTTP response came back (e.g. `request_timeout`).
    WebhookEndpoint:
      type: object
      required:
        - id
        - url
        - events
        - prefix
        - checksum
        - status
        - failure_count
        - created_at
      properties:
        id:
          type: string
          description: Stable identifier for the endpoint.
        url:
          type: string
          format: uri
          description: HTTPS URL Humind will POST events to.
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
        prefix:
          type: string
          description: >-
            Public identifier of the secret (`whsec_<22 base62>`). The full secret is only returned at create / rotate
            time.
        checksum:
          type: string
          description: 8-char CRC32 hex of the prefix.
        status:
          type: string
          enum:
            - active
            - disabled
          description: |
            `disabled` when the merchant disabled it manually, OR after 5
            consecutive permanently-failed deliveries (auto-disable). PATCH
            with `{ "status": "active" }` to re-enable.
        failure_count:
          type: integer
          minimum: 0
          description: Consecutive permanent delivery failures since the last success. Resets to 0 on any 2xx delivery.
        created_at:
          type: string
          format: date-time
        last_delivered_at:
          type: string
          format: date-time
          nullable: true
        last_failed_at:
          type: string
          format: date-time
          nullable: true
        disabled_at:
          type: string
          format: date-time
          nullable: true
        disabled_reason:
          type: string
          nullable: true
          description: e.g. `manual_disable` or `consecutive_failures`.
    WebhookEndpointCreateRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          description: >-
            HTTPS endpoint to POST events to. In production, must be `https://`, must not target localhost or a private
            IP.
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventType'
    WebhookEndpointCreateResponse:
      type: object
      required:
        - endpoint
        - secret
      properties:
        endpoint:
          $ref: '#/components/schemas/WebhookEndpoint'
        secret:
          type: string
          description: |
            Cleartext webhook signing secret in the form
            `whsec_<22 base62>_<8 hex CRC32>`. Returned **once** at
            creation. Store it on your side — Humind cannot retrieve it
            again. Use it to verify each request via
            `HMAC_SHA256(secret, "<timestamp>.<raw-body>")`.
    WebhookEndpointPatch:
      type: object
      description: At least one of `url`, `events`, `status` must be present.
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventType'
        status:
          type: string
          enum:
            - active
            - disabled
    WebhookEndpointRotateResponse:
      type: object
      required:
        - endpoint
        - secret
      properties:
        endpoint:
          $ref: '#/components/schemas/WebhookEndpoint'
        secret:
          type: string
          description: New cleartext secret. The previous one is immediately invalidated; there is no grace period.
    WebhookEventType:
      type: string
      enum:
        - import.completed
        - import.failed
      description: Event types emitted by Humind. New events may be added later.
