openapi: 3.1.0
info:
  title: Xberg Backend API
  description: Control-plane API for Xberg Enterprise deployments.
  contact:
    name: Xberg Enterprise
    url: https://example.com
    email: admin@example.com
  license:
    name: LicenseRef-Proprietary
    identifier: LicenseRef-Proprietary
  version: 0.1.0
paths:
  /auth/account:
    delete:
      tags:
        - auth
      summary: Delete the authenticated user's account and all associated data
      description: |-
        Soft-deletes all projects owned by the user, revokes their API keys,
        soft-deletes their webhooks, and removes all project memberships.
      operationId: delete_account
      responses:
        "204":
          description: Account deleted successfully
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: API-key cache unreachable, retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /auth/config:
    get:
      tags:
        - auth
      summary: '`GET /auth/config`: which auth methods this instance accepts.'
      description: |-
        Unauthenticated: the frontend reads it before any sign-in to decide
        whether to render an OIDC button. Xberg Pro serves the same route with the
        same shape.
      operationId: get_auth_config
      responses:
        "200":
          description: Authentication configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthConfigResponse'
  /auth/login:
    post:
      tags:
        - auth
      summary: Authenticate with an OIDC ID token and receive a backend JWT
      description: Validates the configured identity-provider token, creates the user's first project when needed, and returns the active project in a backend session token.
      operationId: login
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
        required: true
      responses:
        "200":
          description: Successful login
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid OIDC ID token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Email domain is not permitted to sign in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Authentication is disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Identity provider service unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /auth/profile:
    get:
      tags:
        - auth
      summary: Return the authenticated user's profile.
      description: Reads the durable display name for the session's OIDC subject without requiring an active project.
      operationId: get_user_profile
      responses:
        "200":
          description: Authenticated user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfileResponse'
        "401":
          description: A user session is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - auth
      summary: Replace the authenticated user's display name.
      description: Trims and validates the supplied name, then stores it independently of project membership.
      operationId: update_user_profile
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserProfileRequest'
        required: true
      responses:
        "200":
          description: Updated user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfileResponse'
        "400":
          description: Display name is empty or too long
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: A user session is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /auth/saml/acs:
    post:
      tags:
        - saml
      description: Validates a solicited SAML response and returns a short-lived browser exchange code.
      operationId: acs
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SamlAcsForm'
        required: true
      responses:
        "303":
          description: Redirect with a one-time exchange code
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid SAML response
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /auth/saml/exchange:
    post:
      tags:
        - saml
      description: Consumes a single-use SAML exchange code and returns the authenticated browser session token.
      operationId: exchange
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SamlExchangeRequest'
        required: true
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SamlExchangeResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Expired or consumed exchange code
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /auth/saml/login:
    get:
      tags:
        - saml
      description: Discovers a project identity provider by email domain and starts a signed SAML login.
      operationId: login
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
        - name: redirect
          in: query
          required: false
          schema:
            type: string
      responses:
        "303":
          description: Redirect to the discovered IdP
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: No verified SAML domain
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /auth/saml/metadata:
    get:
      tags:
        - saml
      description: Returns the service provider metadata used to configure a SAML identity provider.
      operationId: metadata
      responses:
        "200":
          description: SP metadata XML
        "404":
          description: SAML is not configured
  /healthz:
    get:
      tags:
        - health
      summary: Liveness probe - returns 200 if the process is running
      description: Reports process liveness without checking dependencies, so orchestrators can restart a stopped or wedged process.
      operationId: healthz
      responses:
        "200":
          description: Service is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                tier: enterprise
                auto_tune:
                  enabled: true
                rag:
                  enabled: true
  /readyz:
    get:
      tags:
        - health
      summary: Readiness probe - returns 200 if the service can handle traffic
      description: Checks both application database pools and returns 503 while either dependency cannot serve requests.
      operationId: readyz
      responses:
        "200":
          description: Service is ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
              example:
                status: ready
                checks:
                  database: ok
                  maintenance_database: ok
        "503":
          description: Service is not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
  /scim/v2/{connection_id}/Groups:
    get:
      tags:
        - scim
      description: List and filter groups provisioned through a SCIM connection
      operationId: list_scim_groups
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: filter
          in: query
          description: SCIM equality filter
          required: false
          schema:
            type: string
        - name: startIndex
          in: query
          description: One-based result offset
          required: false
          schema:
            type: integer
            format: int64
        - name: count
          in: query
          description: Maximum results to return
          required: false
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: SCIM group list
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid filter or pagination
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    post:
      tags:
        - scim
      description: Provision a group through a SCIM connection
      operationId: create_scim_group
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimGroupInput'
        required: true
      responses:
        "201":
          description: SCIM group created
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM group
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "409":
          description: SCIM group already exists
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/Groups/{group_id}:
    get:
      tags:
        - scim
      description: Retrieve a provisioned SCIM group
      operationId: get_scim_group
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: group_id
          in: path
          description: SCIM group ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: SCIM group
          content:
            application/scim+json:
              schema: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM group not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    put:
      tags:
        - scim
      description: Replace a provisioned SCIM group
      operationId: replace_scim_group
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: group_id
          in: path
          description: SCIM group ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimGroupInput'
        required: true
      responses:
        "200":
          description: SCIM group updated
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM group update
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM group not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "412":
          description: SCIM group version mismatch
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    delete:
      tags:
        - scim
      description: Delete a provisioned group from a SCIM connection
      operationId: delete_scim_group
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: group_id
          in: path
          description: SCIM group ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: SCIM group deleted
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM group not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    patch:
      tags:
        - scim
      description: Apply SCIM patch operations to a provisioned group
      operationId: patch_scim_group
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: group_id
          in: path
          description: SCIM group ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimPatchRequest'
        required: true
      responses:
        "200":
          description: SCIM group updated
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM group update
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM group not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "412":
          description: SCIM group version mismatch
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/ResourceTypes:
    get:
      tags:
        - scim
      description: List the SCIM resource types supported by this connection
      operationId: resource_types
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Supported SCIM resource types
          content:
            application/scim+json:
              schema: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/Schemas:
    get:
      tags:
        - scim
      description: List the SCIM schemas supported by this connection
      operationId: schemas
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Supported SCIM schemas
          content:
            application/scim+json:
              schema: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/ServiceProviderConfig:
    get:
      tags:
        - scim
      description: Describe the SCIM protocol features supported by this connection
      operationId: service_provider_config
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: SCIM service provider configuration
          content:
            application/scim+json:
              schema: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/Users:
    get:
      tags:
        - scim
      description: List and filter users provisioned through a SCIM connection
      operationId: list_scim_users
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: filter
          in: query
          description: SCIM equality filter
          required: false
          schema:
            type: string
        - name: startIndex
          in: query
          description: One-based result offset
          required: false
          schema:
            type: integer
            format: int64
        - name: count
          in: query
          description: Maximum results to return
          required: false
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: SCIM user list
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid filter or pagination
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    post:
      tags:
        - scim
      description: Provision a user through a SCIM connection
      operationId: create_scim_user
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimUserInput'
        required: true
      responses:
        "201":
          description: SCIM user created
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM user
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "409":
          description: SCIM user already exists
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/Users/{user_id}:
    get:
      tags:
        - scim
      description: Retrieve a provisioned SCIM user
      operationId: get_scim_user
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: SCIM user ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: SCIM user
          content:
            application/scim+json:
              schema: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM user not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    put:
      tags:
        - scim
      description: Replace all attributes of a provisioned SCIM user
      operationId: replace_scim_user
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: SCIM user ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimUserInput'
        required: true
      responses:
        "200":
          description: SCIM user updated
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM user update
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM user not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "412":
          description: SCIM user version mismatch
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    delete:
      tags:
        - scim
      description: Deactivate a provisioned SCIM user
      operationId: delete_scim_user
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: SCIM user ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: SCIM user deactivated
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM user not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "412":
          description: SCIM user changed concurrently
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
    patch:
      tags:
        - scim
      description: Apply SCIM patch operations to a provisioned user
      operationId: patch_scim_user
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: SCIM user ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/scim+json:
            schema:
              $ref: '#/components/schemas/ScimPatchRequest'
        required: true
      responses:
        "200":
          description: SCIM user updated
          content:
            application/scim+json:
              schema: {}
        "400":
          description: Invalid SCIM user update
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "401":
          description: Invalid or expired SCIM bearer token
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "404":
          description: SCIM user not found
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "412":
          description: SCIM user version mismatch
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: SCIM media type required
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: SCIM operation failed
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/ScimErrorBody'
      security:
        - bearer_auth: []
  /scim/v2/{connection_id}/oauth2/token:
    post:
      tags:
        - scim
      description: Issue a SCIM bearer token using OAuth 2.0 client credentials
      operationId: issue_scim_token
      parameters:
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ScimTokenRequest'
        required: true
      responses:
        "200":
          description: Bearer token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScimTokenResponse'
        "400":
          description: Unsupported OAuth grant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorBody'
        "401":
          description: Invalid SCIM client credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorBody'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: Token issuance failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorBody'
  /v1/invitations/accept:
    post:
      tags:
        - members
      summary: Accept an invitation using a token
      description: Consumes a valid invitation token, adds the authenticated user to the project, and returns the resulting membership.
      operationId: accept_invitation
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AcceptInvitationRequest'
        required: true
      responses:
        "200":
          description: Invitation accepted, membership created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberResponse'
        "400":
          description: Invalid or expired token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: User is already a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/oauth/callback:
    get:
      tags:
        - oauth
      summary: OAuth callback handler
      description: |-
        This is a **public** endpoint that doesn't require authentication.
        It processes the authorization code and state from the OAuth provider.
        Returns 503 if OAuth is not configured.
      operationId: oauth_callback
      parameters:
        - name: code
          in: query
          description: Authorization code from provider
          required: true
          schema:
            type: string
        - name: state
          in: query
          description: State token for CSRF protection
          required: true
          schema:
            type: string
      responses:
        "303":
          description: Redirect to the landing page, on success and on a provider-reported denial
          headers:
            Location:
              schema:
                type: string
              description: Dashboard landing URL
        "400":
          description: Invalid or missing parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: OAuth authentication refused
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "500":
          description: OAuth processing error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: OAuth not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects:
    get:
      tags:
        - projects
      summary: List all projects for the authenticated user
      description: Returns only projects visible to the authenticated user, using limit and offset to page through memberships.
      operationId: list_projects
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
      responses:
        "200":
          description: List of projects
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListProjectsResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - projects
      summary: Create a new project
      description: Creates the project, owner membership, billing account, and audit record in one transaction; the authenticated user becomes the owner.
      operationId: create_project
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
        required: true
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Project slug already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}:
    get:
      tags:
        - projects
      summary: Get project details by ID
      description: Requires project membership and returns identity, ownership, status, timestamps, and the response's aggregate counters.
      operationId: get_project
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Project details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - projects
      summary: Delete a project
      description: |-
        Requires project ownership and tombstones project rows. With `erase=true`, it requests asynchronous content erasure. The
        endpoint still returns 204 if event publication fails; an operator must retry erasure.
      operationId: delete_project
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: erase
          in: query
          description: |-
            Erase the project's content, not only its rows.

            Without it the delete is what this route has always done: the rows are
            tombstoned and the caches are purged. With `erase=true` the stored
            documents, the cached results, the vector chunks and the retrieval
            audit rows go as well, and none of it comes back.
          required: false
          schema:
            type: boolean
      responses:
        "204":
          description: Project deleted
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: API-key cache unreachable, retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - projects
      summary: Update project details
      description: Requires project ownership and applies the supplied mutable fields while retaining values omitted from the request.
      operationId: update_project
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProjectRequest'
        required: true
      responses:
        "200":
          description: Project updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/analytics:
    get:
      tags:
        - analytics
      summary: Get analytics data for a project within a date range
      description: |-
        Returns daily usage metrics and processing time percentiles aggregated
        from extraction jobs. Includes job counts, pages/tables/images extracted,
        file type breakdowns, and p50/p95/p99 processing latencies.
      operationId: get_analytics
      parameters:
        - name: start_date
          in: query
          description: Start date (ISO 8601 format, e.g., "2025-01-01")
          required: true
          schema:
            type: string
          example: 2025-01-01
        - name: end_date
          in: query
          description: End date (ISO 8601 format, e.g., "2025-01-31")
          required: true
          schema:
            type: string
          example: 2025-01-31
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsResponse'
        "400":
          description: Invalid date format or range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/api-keys:
    get:
      tags:
        - api_keys
      summary: List all API keys for a project
      description: Requires project membership and returns redacted key metadata; secret key values are never returned by this operation.
      operationId: list_api_keys
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListApiKeysResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - api_keys
      summary: Create a new API key for a project
      description: Requires project ownership, persists a hashed credential, and returns the plaintext key once in the creation response.
      operationId: create_api_key
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
        required: true
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/api-keys/{key_id}:
    delete:
      tags:
        - api_keys
      summary: Revoke an API key
      description: Requires project ownership and permanently disables the selected credential for subsequent API authentication.
      operationId: revoke_api_key
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: key_id
          in: path
          description: API Key ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: API key revoked
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: API-key cache unreachable, retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/api-keys/{key_id}/regenerate:
    post:
      tags:
        - api_keys
      summary: Regenerate an API key
      description: Requires project ownership, revokes the existing credential, and returns its replacement plaintext key once.
      operationId: regenerate_api_key
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: key_id
          in: path
          description: API Key ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: API key regenerated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: API key is revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: API-key cache unreachable, retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/audit:
    get:
      tags:
        - audit
      summary: List a project's audit events, most recent first.
      description: Requires project membership and returns project audit entries in reverse chronological order with limit and offset pagination.
      operationId: list_project_audit
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: action
          in: query
          description: |-
            Filter to entries matching a known audit action exactly (e.g. `"job.submit"`).
            Unknown actions are rejected with a 400 response.
          required: false
          schema:
            type: string
            maxLength: 64
        - name: limit
          in: query
          description: Page size. Values outside 1..=100 are rejected.
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Number of items to skip. Values outside 0..=1,000,000 are rejected.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
      responses:
        "200":
          description: Audit entries for the project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAuditEntriesResponse'
        "400":
          description: Invalid action or pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/billing:
    get:
      tags:
        - billing
      summary: Get billing and quota information for a project
      description: |-
        Returns the project's plan limits, current usage for the billing period,
        and remaining quota. Useful for rendering usage progress bars on the
        frontend dashboard.
      operationId: get_billing
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Billing and quota information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/billing/checkout:
    post:
      tags:
        - billing
      summary: Create a Stripe Checkout Session for a project
      description: |-
        Creates (or reuses) a Stripe customer for the project and opens a
        Checkout Session in subscription mode so the user can add a payment
        method and subscribe to the standard plan.
      operationId: create_checkout
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Checkout Session URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Subscription already active
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/billing/portal:
    post:
      tags:
        - billing
      summary: Create a Stripe Customer Portal Session for a project
      description: |-
        Opens a Customer Portal session so the user can manage their subscription,
        update payment methods, or view billing history.
      operationId: create_portal
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Customer Portal URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalResponse'
        "400":
          description: No billing account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations:
    get:
      tags:
        - integrations
      summary: List all integrations for a project
      description: Requires project membership and returns configured integrations in a paginated response without exposing stored credentials.
      operationId: list_integrations
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of integrations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListIntegrationsResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - integrations
      summary: Create a new integration
      description: Requires project ownership, validates provider configuration, and seals OAuth or OIDC credentials before persistence.
      operationId: create_integration
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateIntegrationRequest'
        required: true
      responses:
        "201":
          description: Integration created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Integration name already exists in this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations/{iid}:
    get:
      tags:
        - integrations
      summary: Get a specific integration
      description: Requires project membership and returns one integration only when it belongs to the selected project.
      operationId: get_integration
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Integration details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - integrations
      summary: Delete an integration
      description: Requires project ownership and permanently removes the integration after verifying that it belongs to the project.
      operationId: delete_integration
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Integration deleted
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations/{iid}/connect:
    post:
      tags:
        - oauth
      summary: Begin an OAuth authorization flow for an integration (owner-gated).
      description: |-
        Returns the provider authorize URL the client should redirect the user to.
        Returns 503 if OAuth is not configured on this deployment.
      operationId: oauth_connect
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Authorization URL to redirect the user to
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BeginConnectResponse'
        "400":
          description: Integration or provider not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Caller is not a project owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: OAuth not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects/{id}/integrations/{iid}/disconnect:
    post:
      tags:
        - integrations
      summary: Disconnect an integration (revoke OAuth connection)
      description: Requires project ownership and clears the integration's active connection credentials while retaining its configuration record.
      operationId: disconnect_integration
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Integration disconnected
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations/{iid}/documents:
    get:
      tags:
        - documents
      summary: List documents in a connected integration
      description: |-
        Requires the calling user to be a member of the project. The integration
        must be configured with a valid OAuth token or credential.
      operationId: list_integration_documents
      parameters:
        - name: mime_types
          in: query
          description: |-
            Comma-separated MIME types to filter by (e.g., "text/plain,application/pdf").
            If not provided, all document types are returned.
          required: false
          schema:
            type: string
        - name: folder_id
          in: query
          description: |-
            Source-specific folder ID to restrict results to (e.g., Google Drive folder ID).
            If not provided, lists documents from the root/default location.
          required: false
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum number of documents to return.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
        - name: offset
          in: query
          description: Number of matching documents to skip.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of documents
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDocumentsResponse'
        "400":
          description: Invalid query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Session authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: The source connection is missing or revoked and must be connected again
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limited by source
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "502":
          description: Source API error or unsupported document type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations/{iid}/documents/{doc_id}:
    get:
      tags:
        - documents
      summary: Fetch a single document from a connected integration
      description: |-
        Returns the raw document bytes with an appropriate Content-Type header.
        For native source types without a standard export (e.g., Google Docs),
        the connector exports to a usable format (e.g., PDF).
      operationId: fetch_integration_document
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          description: Integration ID
          required: true
          schema:
            type: string
            format: uuid
        - name: doc_id
          in: path
          description: Source-specific document ID
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Document content
          content:
            application/octet-stream: {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Session authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project, integration, or document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: The source connection is missing or revoked and must be connected again
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: Document exceeds maximum size
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limited by source
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "502":
          description: Source API error or unsupported document type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/integrations/{iid}/sync:
    get:
      tags:
        - integrations
      summary: Report the durable synchronization state for one source integration.
      description: Requires integration-read permission and returns the persisted per-integration opt-in state.
      operationId: get_integration_sync
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Synchronization preference
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationSyncResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    put:
      tags:
        - integrations
      summary: Enable or disable durable synchronization for one source integration.
      description: Requires integration-connect permission and preserves the choice across scheduler restarts.
      operationId: set_integration_sync
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: iid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetIntegrationSyncRequest'
        required: true
      responses:
        "204":
          description: Synchronization preference updated
        "400":
          description: Integration kind does not support durable synchronization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/invitations:
    get:
      tags:
        - members
      summary: List pending invitations for a project
      description: Requires project membership and returns pending invitations using limit and offset pagination.
      operationId: list_invitations
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of pending invitations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListInvitationsResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - members
      summary: Create a project invitation (sends token for email delivery by frontend)
      description: Requires project ownership and creates a pending invitation for the requested email address and role.
      operationId: invite_user
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvitationRequest'
        required: true
      responses:
        "201":
          description: Invitation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateInvitationResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Pending invitation already exists for this email, or user is already an active member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/invitations/{inv_id}:
    delete:
      tags:
        - members
      summary: Revoke a pending invitation
      description: Requires project ownership and removes a pending invitation before it can be accepted.
      operationId: revoke_invitation
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: inv_id
          in: path
          description: Invitation ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Invitation revoked
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Invitation not found or not pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/leave:
    post:
      tags:
        - members
      summary: 'Self-service: caller leaves a project they belong to'
      description: Removes the authenticated user's own membership, but rejects a sole owner's request while other members remain.
      operationId: leave_project
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Successfully left project
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - caller is not a member of this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Cannot leave - caller is sole owner with other members; transfer ownership first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/members:
    get:
      tags:
        - members
      summary: List all members of a project
      description: Requires project membership and returns current members with their roles using limit and offset pagination.
      operationId: list_members
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListMembersResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/members/{user_id}:
    delete:
      tags:
        - members
      summary: Remove a member from a project
      description: Requires project ownership and removes the selected member while preventing removal that would violate ownership constraints.
      operationId: remove_member
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: User ID of the member
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Member removed
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or member not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - members
      summary: Update a member's role in a project
      description: Requires project ownership and changes the selected member's role while preserving the project's ownership constraints.
      operationId: update_member_role
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          description: User ID of the member
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateMemberRoleRequest'
        required: true
      responses:
        "200":
          description: Member role updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or member not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/members/{user_id}/custom-role:
    patch:
      tags:
        - roles
      description: Replace the additive custom roles for an existing MEMBER project membership.
      operationId: assign_project_role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: user_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignProjectRoleRequest'
        required: true
      responses:
        "204": {}
        "400":
          description: The target membership cannot receive a custom role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot assign this role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The member or project role does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/rag-config:
    get:
      tags:
        - projects
      summary: Read a project's RAG configuration.
      description: |-
        Xberg Pro serves the same route with the same shape; a client written
        against either tier reads the other.
      operationId: get_rag_config
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Project RAG configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RagConfigResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Not a member of the project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    put:
      tags:
        - projects
      summary: Set a project's RAG configuration.
      description: |-
        Requires project membership and fully replaces the stored `enabled` and `default_collection` values. This operation stores
        configuration; it does not run retrieval.
      operationId: set_rag_config
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetRagConfigRequest'
        required: true
      responses:
        "200":
          description: Updated project RAG configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RagConfigResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Not a member of the project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/roles:
    get:
      tags:
        - roles
      description: List the custom authorization roles configured for a project.
      operationId: list_project_roles
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListProjectRolesResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot read project roles
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - roles
      description: Create a custom project role with an explicit set of permissions.
      operationId: create_project_role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRoleRequest'
        required: true
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRoleResponse'
        "400":
          description: The role definition is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot create this role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: A role with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/roles/{role_id}:
    get:
      tags:
        - roles
      description: Retrieve one custom project role and its exact permission set.
      operationId: get_project_role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: role_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRoleResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot read project roles
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The project role does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    delete:
      tags:
        - roles
      description: Delete a custom project role and clear it from members and invitations.
      operationId: delete_project_role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: role_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204": {}
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot delete this role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The project role does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - roles
      description: Replace a custom project role's name, description, and permissions.
      operationId: update_project_role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: role_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProjectRoleRequest'
        required: true
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRoleResponse'
        "400":
          description: The role definition is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Authentication is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: The caller cannot update this role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: The project role does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: A role with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/sandbox/extract:
    post:
      tags:
        - sandbox
      summary: Extract a document in the sandbox (first page only)
      description: |-
        Accepts a single file upload via multipart form data, proxies the extraction
        request through the REST API service, and returns the first page of results.
        This gives users a realistic preview of the extraction pipeline.
      operationId: sandbox_extract
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          multipart/form-data: {}
      responses:
        "200":
          description: Extraction result (first page)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxExtractResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "408":
          description: Extraction timed out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limited (upstream)
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Sandbox not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/scim/connections:
    get:
      tags:
        - scim
      description: List the project's SCIM provisioning connections without returning client secrets.
      operationId: list_scim_connections
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: SCIM connections
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListScimConnectionsResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Insufficient project permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - scim
      description: Create the project's SCIM provisioning connection and return its client secret once.
      operationId: create_scim_connection
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScimConnectionRequest'
        required: true
      responses:
        "201":
          description: SCIM connection created; client secret is returned once
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateScimConnectionResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Insufficient project permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/scim/connections/{connection_id}/groups:
    get:
      tags:
        - scim
      description: List provisioned SCIM groups and the project roles currently mapped to each group.
      operationId: list_scim_admin_groups
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Provisioned groups and mapped roles
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListScimAdminGroupsResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/scim/connections/{connection_id}/groups/{group_id}/roles:
    put:
      tags:
        - scim
      description: Replace the complete project-role mapping for one provisioned SCIM group.
      operationId: replace_scim_group_roles
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
        - name: group_id
          in: path
          description: SCIM group ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplaceScimGroupRolesRequest'
        required: true
      responses:
        "204":
          description: Group role mappings replaced
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Owner role required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/scim/connections/{connection_id}/rotate:
    post:
      tags:
        - scim
      description: Rotate a SCIM connection credential and revoke every access token issued before rotation.
      operationId: rotate_scim_connection_secret
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: connection_id
          in: path
          description: SCIM connection ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Credential rotated and prior access tokens revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateScimConnectionResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: SCIM connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/usage:
    get:
      tags:
        - usage
      summary: Get usage statistics for a project within a date range
      description: Requires project membership and aggregates request counts, transfer sizes, and latency across the inclusive date window.
      operationId: get_usage
      parameters:
        - name: start_date
          in: query
          description: Start date for the usage period (ISO 8601 format, e.g., "2025-01-01")
          required: true
          schema:
            type: string
          example: 2025-01-01
        - name: end_date
          in: query
          description: End date for the usage period (ISO 8601 format, e.g., "2025-01-31")
          required: true
          schema:
            type: string
          example: 2025-01-31
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        "400":
          description: Invalid date format or range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks:
    get:
      tags:
        - webhooks
      summary: List all webhooks for a project
      description: Requires project membership and returns webhook subscriptions with recent delivery statistics using limit and offset pagination.
      operationId: list_webhooks
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of webhooks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhooksResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    post:
      tags:
        - webhooks
      summary: Create a new webhook for a project
      description: Requires project ownership, validates the HTTPS endpoint and event set, and stores the signing secret used for deliveries.
      operationId: create_webhook
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
        required: true
      responses:
        "201":
          description: Webhook created; signing secret is shown only once
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateWebhookResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks/{wh_id}:
    delete:
      tags:
        - webhooks
      summary: Delete a webhook
      description: Requires project ownership and soft-deactivates the subscription by recording its deletion timestamp.
      operationId: delete_webhook
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          description: Webhook ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Webhook deleted
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
    patch:
      tags:
        - webhooks
      summary: Update a webhook's configuration
      description: Requires project ownership and updates supplied subscription fields after revalidating endpoint and event constraints.
      operationId: update_webhook
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          description: Webhook ID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
        required: true
      responses:
        "200":
          description: Webhook updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks/{wh_id}/deliveries:
    get:
      tags:
        - webhooks
      summary: List managed webhook attempts eligible for cleanup after seven days, newest first.
      description: Requires project membership and returns retained attempt metadata without request or response payload previews, using limit and offset pagination.
      operationId: list_webhook_deliveries
      parameters:
        - name: limit
          in: query
          description: |-
            Page size. A value outside 1..=1000 is rejected with a 400, on both
            tiers.
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            maximum: 1000
            minimum: 1
          example: 100
        - name: offset
          in: query
          description: |-
            Number of items to skip. A negative value is rejected with a 400; a
            value above 1,000,000 is accepted but clamped to 1,000,000, which
            selects an empty page rather than erroring.
          required: false
          schema:
            type: integer
            format: int64
            default: 0
            maximum: 1000000
            minimum: 0
          example: 0
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          description: Webhook ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Paginated delivery history for the webhook
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhookDeliveriesResponse'
        "400":
          description: Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks/{wh_id}/deliveries/{delivery_id}:
    get:
      tags:
        - webhooks
      summary: Fetch bounded payload previews for one retained managed attempt.
      description: Requires project membership and returns one retained attempt with bounded request and response previews; truncation flags identify incomplete previews.
      operationId: get_webhook_delivery
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: delivery_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Delivery details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryDetailResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Not a project member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Webhook or delivery not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks/{wh_id}/deliveries/{delivery_id}/retry:
    post:
      tags:
        - webhooks
      summary: Manually retry one past delivery attempt.
      description: Queue the original canonical payload for delivery to the subscription's current endpoint.
      operationId: retry_webhook_delivery
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          description: Webhook ID
          required: true
          schema:
            type: string
            format: uuid
        - name: delivery_id
          in: path
          description: Delivery attempt ID to redeliver
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          description: Optional project-scoped replay identity, at most 128 ASCII bytes
          required: false
          schema:
            type:
              - string
              - "null"
      responses:
        "202":
          description: Retry accepted and queued for redelivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryWebhookDeliveryResponse'
        "400":
          description: Invalid idempotency key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project, webhook, or delivery not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: Incomplete source, inactive subscription, or idempotency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Replay limit reached; Retry-After indicates when to retry
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{id}/webhooks/{wh_id}/test:
    post:
      tags:
        - webhooks
      summary: Send a test delivery to a webhook endpoint
      description: Requires project ownership and performs a test delivery to the configured endpoint, returning the receiver outcome.
      operationId: test_webhook
      parameters:
        - name: id
          in: path
          description: Project ID
          required: true
          schema:
            type: string
            format: uuid
        - name: wh_id
          in: path
          description: Webhook ID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Test delivery result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized - no auth token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "403":
          description: Forbidden - user not owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Project or webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer_auth: []
  /v1/projects/{project_id}/saml/domains:
    get:
      tags:
        - saml
      description: Lists project SAML discovery domains and their DNS verification state.
      operationId: list_domains
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SamlDomainResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - saml
      description: Creates a project SAML discovery domain with a DNS TXT verification challenge.
      operationId: create_domain
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SamlDomainRequest'
        required: true
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SamlDomainResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects/{project_id}/saml/domains/{domain}/verify:
    post:
      tags:
        - saml
      description: Checks the hosted DNS TXT challenge before enabling domain-based SAML discovery.
      operationId: verify_domain
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: domain
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Domain verified
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects/{project_id}/saml/providers:
    get:
      tags:
        - saml
      description: Lists the SAML identity providers configured for a project owner.
      operationId: list_providers
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SamlProviderResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - saml
      description: Creates a project SAML identity provider from bounded uploaded metadata XML.
      operationId: create_provider
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SamlProviderRequest'
        required: true
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SamlProviderResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects/{project_id}/saml/providers/{provider_id}:
    put:
      tags:
        - saml
      description: Replaces a project SAML provider configuration and its uploaded signing metadata.
      operationId: update_provider
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: provider_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SamlProviderRequest'
        required: true
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SamlProviderResponse'
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: The request body is larger than this deployment accepts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - saml
      description: Deletes a project SAML provider and its dependent domains and identities.
      operationId: delete_provider
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: provider_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Provider deleted
        "400":
          description: 'The request is malformed: a path parameter, a query parameter or the body did not parse'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded for this caller. The body is the shared error envelope with code `rate_limited`; `Retry-After` and the `x-ratelimit-*` headers carry the wait.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/sandbox/public/extract:
    post:
      tags:
        - sandbox
      summary: Extract a document from the public landing-page sandbox.
      description: |-
        Public twin of `sandbox_extract`, reachable without dashboard auth. The
        route is always mounted; the protective layers around it are the
        non-negotiable trust boundary:

        1. Body-size cap (`axum::extract::DefaultBodyLimit`).
        2. Trusted-proxy-aware client-IP resolution.
        3. Per-IP rate limit keyed from the resolved client IP.
        4. Optional OIDC token verification (`PublicSandboxAuthMode::Oidc`) — when
           configured, also unlocks the per-UID rate limit layer.
        5. Per-page cap enforced server-side via
           `extraction_config.security_limits.max_pages`.

        The handler proxies to the authenticated REST API service using a single
        long-lived `SANDBOX_PUBLIC_API_KEY`, which is created once at deployment
        time for the synthetic public-sandbox project. No per-request DB writes
        happen here.
      operationId: public_sandbox_extract
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicSandboxExtractForm'
        required: true
      responses:
        "200":
          description: Extraction result (first page)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxExtractResponse'
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Sandbox token rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "408":
          description: Extraction timed out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "413":
          description: Request body too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "415":
          description: 'The request body was not sent with Content-Type: application/json'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until this request may be retried.
            x-ratelimit-after:
              schema:
                type: integer
              description: Seconds until the rate-limit bucket admits another request.
            x-ratelimit-limit:
              schema:
                type: integer
              description: Maximum requests allowed in the current rate-limit burst.
            x-ratelimit-remaining:
              schema:
                type: integer
              description: Requests remaining in the current rate-limit burst.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "503":
          description: Public sandbox unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    AcceptInvitationRequest:
      type: object
      description: Request to accept an invitation using its token.
      required:
        - token
      properties:
        token:
          type: string
          description: Invitation token received via email.
    AnalyticsQuery:
      type: object
      description: Query parameters for analytics endpoint
      required:
        - start_date
        - end_date
      properties:
        end_date:
          type: string
          description: End date (ISO 8601 format, e.g., "2025-01-31")
        start_date:
          type: string
          description: Start date (ISO 8601 format, e.g., "2025-01-01")
    AnalyticsResponse:
      type: object
      description: Analytics response containing daily metrics
      required:
        - project_id
        - start_date
        - end_date
        - days
      properties:
        days:
          type: array
          items:
            $ref: '#/components/schemas/DailyAnalyticsEntry'
          description: Daily analytics entries
        end_date:
          type: string
          description: End date of the analytics period
          example: 2025-01-31
        project_id:
          type: string
          description: The project identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        start_date:
          type: string
          description: Start date of the analytics period
          example: 2025-01-01
    AnnotationKind:
      oneOf:
        - type: object
          description: Bold (strong) text formatting.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - bold
        - type: object
          description: Italic (emphasis) text formatting.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - italic
        - type: object
          description: Underlined text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - underline
        - type: object
          description: Strikethrough text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - strikethrough
        - type: object
          description: Inline code span.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - code
        - type: object
          description: Subscript text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - subscript
        - type: object
          description: Superscript text.
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - superscript
        - type: object
          description: Hyperlink annotation.
          required:
            - url
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - link
            title:
              type:
                - string
                - "null"
              description: Optional link title attribute.
            url:
              type: string
              description: Hyperlink target URL.
        - type: object
          description: Highlighted text (PDF highlights, HTML `<mark>`).
          required:
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - highlight
        - type: object
          description: Text color (CSS-compatible value, e.g. "#ff0000", "red").
          required:
            - value
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - color
            value:
              type: string
              description: CSS-compatible color value (e.g. `"#ff0000"`, `"red"`).
        - type: object
          description: Font size with units (e.g. "12pt", "1.2em", "16px").
          required:
            - value
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - font_size
            value:
              type: string
              description: Font size including unit (e.g. `"12pt"`, `"1.2em"`, `"16px"`).
        - type: object
          description: Extensible annotation for format-specific styling.
          required:
            - name
            - annotation_type
          properties:
            annotation_type:
              type: string
              enum:
                - custom
            name:
              type: string
              description: Name of the custom annotation kind.
            value:
              type:
                - string
                - "null"
              description: Optional value or parameter for the annotation.
      description: Types of inline text annotations.
    ApiKeyResponse:
      type: object
      description: |-
        API key details for list operations. Never includes the full key, only
        the prefix, for security.
      required:
        - id
        - key_prefix
        - scope
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        expires_at:
          type:
            - string
            - "null"
          description: |-
            Optional expiration date in ISO 8601 format. Both tiers reject the key
            after this timestamp.
        id:
          type: string
          format: uuid
          description: Unique API key identifier.
        key_prefix:
          type: string
          description: Key prefix for identification (e.g. `"kz_abcdefgh"`).
        last_used_at:
          type:
            - string
            - "null"
          description: Optional last-used timestamp in ISO 8601 format.
        name:
          type:
            - string
            - "null"
          description: User-provided name for the key, when set.
        revoked_at:
          type:
            - string
            - "null"
          description: Optional revocation timestamp in ISO 8601 format.
        scope:
          $ref: '#/components/schemas/ApiKeyScope'
          description: 'What the key may do: `read` or `write`.'
    ApiKeyScope:
      type: string
      description: |-
        What an API key may do.

        A `Read` key is refused on every request whose HTTP method is not safe.
        The safe methods are GET, HEAD, OPTIONS and TRACE. A `Write` key has full
        access. The rule is the method, not the route, so a read that is shaped as a
        POST needs a `Write` key. The value is stored in `api_keys.scope` and
        carried on the wire in lowercase.
      enum:
        - read
        - write
    ArchiveEntry:
      type: object
      description: |-
        A single file extracted from an archive.

        When archives (ZIP, TAR, 7Z, GZIP) are extracted with recursive extraction
        enabled, each processable file produces its own full `ExtractedDocument`.
      required:
        - path
        - mime_type
        - result
      properties:
        mime_type:
          type: string
          description: Detected MIME type of the file.
        path:
          type: string
          description: Archive-relative file path (e.g. "folder/document.pdf").
        result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: Full extraction result for this file.
    ArchiveMetadata:
      type: object
      description: |-
        Archive (ZIP/TAR/7Z) metadata.

        Extracted from compressed archive files containing file lists and size information.
      required:
        - format
        - file_count
        - file_list
        - total_size
      properties:
        compressed_size:
          type:
            - integer
            - "null"
          format: int64
          description: Compressed size in bytes (if available)
          minimum: 0
        file_count:
          type: integer
          format: int32
          description: Total number of files in the archive
          minimum: 0
        file_list:
          type: array
          items:
            type: string
          description: List of file paths within the archive
        format:
          type: string
          description: Archive format ("ZIP", "TAR", "7Z", etc.)
        total_size:
          type: integer
          format: int64
          description: Total uncompressed size in bytes
          minimum: 0
    AssignProjectRoleRequest:
      type: object
      properties:
        custom_role_id:
          type:
            - string
            - "null"
          format: uuid
        custom_role_ids:
          type:
            - array
            - "null"
          items:
            type: string
            format: uuid
    Attributes:
      type: object
      description: |-
        Element attributes in Djot.

        Represents the attributes attached to elements using {.class #id key="value"} syntax.
      properties:
        classes:
          type: array
          items:
            type: string
          description: CSS classes (.class1 .class2)
        id:
          type:
            - string
            - "null"
          description: Element ID (#identifier)
        key_values:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Key-value pairs (key="value")
    AudioMetadata:
      type: object
      description: |-
        Audio/video file metadata.

        Populated from container tags (ID3v2, MP4 atoms, Vorbis comments, etc.) and
        PCM decode properties. Available when the `transcription-types` feature is enabled.
      properties:
        bitrate:
          type:
            - integer
            - "null"
          format: int32
          description: Audio bitrate in kbps from the source file tags/properties.
          minimum: 0
        channels:
          type:
            - integer
            - "null"
          format: int32
          description: Number of audio channels (1 = mono, 2 = stereo).
          minimum: 0
        codec:
          type:
            - string
            - "null"
          description: Audio codec (e.g. "mp3", "aac", "opus", "flac").
        container:
          type:
            - string
            - "null"
          description: Container format (e.g. "mpeg", "mp4", "ogg", "wav").
        duration_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Duration in milliseconds derived from the decoded audio stream.
          minimum: 0
        sample_rate_hz:
          type:
            - integer
            - "null"
          format: int32
          description: Sample rate in Hz after decode (always 16000 when resampled for Whisper).
          minimum: 0
    AuditEntry:
      type: object
      description: A single customer-facing audit log entry.
      required:
        - id
        - actor
        - action
        - resource_type
        - metadata
        - created_at
      properties:
        action:
          type: string
          description: Action performed (e.g. `"job.submit"`, `"api_key.revoke"`).
        actor:
          type: string
          description: Actor that performed the action (e.g. an API key prefix or user id).
        created_at:
          type: string
          format: date-time
          description: ISO 8601 event timestamp.
        id:
          type: string
          format: uuid
          description: Unique event identifier.
        metadata:
          type: object
          description: Additional structured metadata describing the event.
          additionalProperties: {}
          propertyNames:
            type: string
        resource_id:
          type:
            - string
            - "null"
          description: Identifier of the affected resource, when applicable.
        resource_type:
          type: string
          description: Type of resource the action applied to (e.g. `"job"`, `"api_key"`).
    AuthConfigResponse:
      type: object
      description: '`GET /auth/config` response: which auth methods this instance accepts.'
      required:
        - methods
        - oidc
      properties:
        identity:
          $ref: '#/components/schemas/LocalIdentity'
          description: Local operator identity when authentication is disabled.
        methods:
          type: array
          items:
            type: string
          description: Accepted authentication methods (`"api_key"`, optionally `"oidc"`).
        mode:
          $ref: '#/components/schemas/AuthMode'
          description: Explicit instance authentication policy; older servers require authentication.
        oidc:
          $ref: '#/components/schemas/OidcInfo'
          description: |-
            OIDC-specific detail, used by the frontend to decide whether to render
            a "Sign in with Google" button.
        saml:
          oneOf:
            - $ref: '#/components/schemas/SamlInfo'
              description: |-
                SAML discovery endpoint. Provider details remain project-scoped and are
                disclosed only after a verified email-domain lookup.
            - type: "null"
    AuthMode:
      type: string
      description: Authentication policy advertised by the instance.
      enum:
        - required
        - disabled
    BeginConnectResponse:
      type: object
      description: OAuth authorization initiation response
      required:
        - authorize_url
      properties:
        authorize_url:
          type: string
          description: Authorization URL to redirect the user to
          example: https://accounts.google.com/o/oauth2/v2/auth?...
    BibtexMetadata:
      type: object
      description: BibTeX bibliography metadata.
      required:
        - entry_count
      properties:
        authors:
          type: array
          items:
            type: string
          description: Author names collected across all bibliography entries.
        citation_keys:
          type: array
          items:
            type: string
          description: BibTeX citation keys (e.g. `"knuth1984"`) for all entries.
        entry_count:
          type: integer
          description: Number of entries in the bibliography.
          minimum: 0
        entry_types:
          type:
            - object
            - "null"
          description: Count of entries grouped by BibTeX entry type (e.g. `"article"` → 5).
          additionalProperties:
            type: integer
            minimum: 0
          propertyNames:
            type: string
        year_range:
          $ref: '#/components/schemas/YearRange'
          description: Earliest and latest publication years found in the bibliography.
    BillingAccountInfo:
      type: object
      description: Stripe billing account information
      required:
        - free_pages_remaining
        - free_pages_total
        - has_payment_method
        - price_tier
        - cancel_at_period_end
      properties:
        cancel_at:
          type:
            - string
            - "null"
          description: |-
            ISO 8601 datetime when the subscription is scheduled to cancel.
            Null when no cancellation is pending.
        cancel_at_period_end:
          type: boolean
          description: |-
            True when the user has scheduled a deferred cancellation via the Stripe portal.
            The subscription stays active until cancel_at / period end.
        discount_expires_at:
          type:
            - string
            - "null"
          description: When the current discount expires (null if no active discount)
        free_pages_remaining:
          type: integer
          format: int64
          description: Free pages remaining (lifetime credit)
        free_pages_total:
          type: integer
          format: int64
          description: Total free pages originally granted
        has_payment_method:
          type: boolean
          description: Whether a Stripe payment method is on file
        price_tier:
          type: string
          description: Current price tier (STANDARD or DISCOUNTED)
        subscription_status:
          type:
            - string
            - "null"
          description: |-
            Stripe subscription status ("active", "past_due", "canceled", etc.).
            Null when no subscription exists on the billing account.
    BillingResponse:
      type: object
      description: Billing and quota response for a project
      required:
        - project_id
        - period_start
        - period_end
        - plan
        - usage
      properties:
        billing_account:
          $ref: '#/components/schemas/BillingAccountInfo'
          description: Stripe billing account information (omitted if not provisioned)
        period_end:
          type: string
          description: End of the current billing period (ISO 8601 date)
          example: 2026-05-01
        period_start:
          type: string
          description: Start of the current billing period (ISO 8601 date)
          example: 2026-04-01
        plan:
          $ref: '#/components/schemas/PlanInfo'
          description: Plan limits
        project_id:
          type: string
          description: The project identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        usage:
          $ref: '#/components/schemas/UsageInfo'
          description: Current usage within the billing period
    BlockType:
      type: string
      description: Types of block-level elements in Djot.
      enum:
        - paragraph
        - heading
        - blockquote
        - code_block
        - list_item
        - ordered_list
        - bullet_list
        - task_list
        - definition_list
        - definition_term
        - definition_description
        - div
        - section
        - thematic_break
        - raw_block
        - math_display
    BoundingBox:
      type: object
      description: Bounding box coordinates for element positioning.
      required:
        - x0
        - y0
        - x1
        - y1
      properties:
        x0:
          type: number
          format: double
          description: Left x-coordinate
        x1:
          type: number
          format: double
          description: Right x-coordinate
        y0:
          type: number
          format: double
          description: Bottom y-coordinate
        y1:
          type: number
          format: double
          description: Top y-coordinate
    CellChange:
      type: object
      description: |-
        A single changed cell within a table.

        Defined here (rather than only in `crate::diff`) so `RevisionDelta` can
        reference it unconditionally, without requiring the `diff` Cargo feature.
        `crate::diff` re-exports this type verbatim.
      required:
        - row
        - col
        - from
        - to
      properties:
        col:
          type: integer
          description: Zero-based column index.
          minimum: 0
        from:
          type: string
          description: Value before the change.
        row:
          type: integer
          description: Zero-based row index.
          minimum: 0
        to:
          type: string
          description: Value after the change.
    CheckoutResponse:
      type: object
      description: Response body for a Stripe Checkout Session
      required:
        - url
      properties:
        url:
          type: string
          description: Stripe Checkout Session URL to redirect the user to
    Chunk:
      type: object
      description: |-
        A text chunk with optional embedding and metadata.

        Chunks are created when chunking is enabled in `ExtractionConfig`. Each chunk
        contains the text content, optional embedding vector (if embedding generation
        is configured), and metadata about its position in the document.
      required:
        - content
        - metadata
      properties:
        chunk_type:
          $ref: '#/components/schemas/ChunkType'
          description: |-
            Semantic structural classification of this chunk.

            Assigned by the heuristic classifier based on content patterns and
            heading context. Defaults to `ChunkType::Unknown` when no rule matches.
        content:
          type: string
          description: The text content of this chunk.
        embedding:
          type:
            - array
            - "null"
          items:
            type: number
            format: float
          description: |-
            Optional embedding vector for this chunk.

            Only populated when `EmbeddingConfig` is provided in chunking configuration.
            The dimensionality depends on the chosen embedding model.
        late_interaction:
          $ref: '#/components/schemas/MultiVectorEmbedding'
          description: |-
            Optional ColBERT-style multi-vector (late-interaction) embedding for this chunk.

            Only populated when late-interaction embedding generation is configured for
            chunking. `None` otherwise, including on builds without the `late-interaction`
            feature.

            Uses the crate-root `MultiVectorEmbedding` alias for the same reason
            `sparse_embedding` uses `SparseEmbedding` — see that field's docs.
        metadata:
          $ref: '#/components/schemas/ChunkMetadata'
          description: Metadata about this chunk's position and properties.
        sparse_embedding:
          $ref: '#/components/schemas/SparseEmbedding'
          description: |-
            Optional sparse (SPLADE) learned embedding for this chunk.

            Only populated when sparse-embedding generation is configured for chunking.
            `None` otherwise, including on builds without the `sparse-embeddings` feature.

            Uses the crate-root `SparseEmbedding` alias rather than
            `crate::sparse_embeddings::SparseEmbedding` directly: the `sparse_embeddings`
            module itself only compiles under `sparse-embeddings`/`sparse-embedding-presets`,
            while the crate-root alias is always defined (a field-compatible stub on builds
            without either feature), so this field — and `Chunk` itself — compiles on every
            feature combination, including the crate's default features.
    ChunkMetadata:
      type: object
      description: Metadata about a chunk's position in the original document.
      required:
        - byte_start
        - byte_end
        - chunk_index
        - total_chunks
      properties:
        byte_end:
          type: integer
          description: Byte offset where this chunk ends in the original text (UTF-8 valid boundary).
          minimum: 0
        byte_start:
          type: integer
          description: Byte offset where this chunk starts in the original text (UTF-8 valid boundary).
          minimum: 0
        chunk_index:
          type: integer
          description: Zero-based index of this chunk in the document.
          minimum: 0
        classifications:
          type: array
          items:
            $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Multi-label classification result for this chunk.

            Populated by the chunk-classification post-processor when
            `ExtractionConfig.chunk_classification`
            is set. A chunk may match zero, one, or many of the configured label
            definitions. Empty when chunk classification was not configured.
        first_page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            First page number this chunk spans (1-indexed).

            Only populated when page tracking is enabled in extraction configuration.
          minimum: 0
        heading_context:
          $ref: '#/components/schemas/HeadingContext'
          description: |-
            Heading context when using Markdown chunker.

            Contains the heading hierarchy this chunk falls under.
            Only populated when `ChunkerType::Markdown` is used.
        heading_path:
          type: array
          items:
            type: string
          description: |-
            Flattened heading trail from document root to this chunk's section.

            Each element is a heading's text, outermost first. Derived from
            `heading_context` when present; empty otherwise.
            Provides a binding-friendly, RAG-shaped breadcrumb without requiring
            callers to walk the nested `HeadingContext` structure.
        image_indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Indices into `ExtractedDocument.images` for images on pages covered by this chunk.

            Contains zero-based indices into the top-level `images` collection for every
            image whose `page_number` falls within `[first_page, last_page]`.
            Empty when image extraction is disabled or the chunk spans no pages with images.
        last_page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Last page number this chunk spans (1-indexed, equal to first_page for single-page chunks).

            Only populated when page tracking is enabled in extraction configuration.
          minimum: 0
        node_ids:
          type: array
          items:
            type: string
          description: |-
            Ids of the `DocumentNode`s
            this chunk was derived from.

            Joins a chunk back to the structured document tree via
            `DocumentNode.id`.
            Populated from exact node provenance when available, with a textual
            containment fallback for rendered chunks that do not retain byte offsets.
        page_spans:
          type: array
          items:
            $ref: '#/components/schemas/PageSpan'
          description: |-
            Per-page bounding-box spans this chunk covers, for viewer highlighting (#1295).

            One entry per page the chunk overlaps, in page order — the first and last entries'
            `page` fields equal `first_page`/`last_page`.
            Populated whenever page-boundary provenance is available (the same condition under
            which `first_page`/`last_page` are populated); each entry's `bbox` is additionally
            populated when the document's structured node tree (`ExtractedDocument.document`) is
            available, as the union of that page's body-layer node bounding boxes found within this
            chunk. Empty when page-boundary provenance is unavailable (mirrors `first_page`/
            `last_page` being `None`).
        token_count:
          type:
            - integer
            - "null"
          description: |-
            Number of tokens in this chunk (if available).

            This is calculated by the embedding model's tokenizer if embeddings are enabled.
          minimum: 0
        total_chunks:
          type: integer
          description: Total number of chunks in the document.
          minimum: 0
    ChunkType:
      type: string
      description: |-
        Semantic structural classification of a text chunk.

        Assigned by the heuristic classifier in `chunking::classifier`.
        Defaults to `Unknown` when no rule matches.
        Designed to be extended in future versions without breaking changes.
      enum:
        - heading
        - party_list
        - definitions
        - operative_clause
        - signature_block
        - schedule
        - table_like
        - formula
        - code_block
        - function
        - class
        - module
        - image
        - org_chart
        - diagram
        - unknown
    CitationMetadata:
      type: object
      description: Citation file metadata (RIS, PubMed, EndNote).
      required:
        - citation_count
      properties:
        authors:
          type: array
          items:
            type: string
          description: Author names collected across all citation records.
        citation_count:
          type: integer
          description: Total number of citation records in the file.
          minimum: 0
        dois:
          type: array
          items:
            type: string
          description: DOI identifiers found in the citation records.
        format:
          type:
            - string
            - "null"
          description: Detected citation file format (e.g. `"ris"`, `"pubmed"`, `"endnote"`).
        keywords:
          type: array
          items:
            type: string
          description: Keywords collected from all citation records.
        year_range:
          $ref: '#/components/schemas/YearRange'
          description: Earliest and latest publication years found in the file.
    ClassificationLabel:
      type: object
      description: A single label + confidence pair.
      required:
        - label
      properties:
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Backend-reported confidence in `[0.0, 1.0]`. `None` when the backend (e.g. an LLM
            prompt without explicit confidence schema) did not report one.
        label:
          type: string
          description: Label name as configured in `PageClassificationConfig::labels`.
    CodeChunkInfo:
      type: object
      description: |-
        A single structurally-meaningful code chunk produced by tree-sitter parsing.

        Purpose-built payload owned by xberg — deliberately does not expose the upstream
        `tree_sitter_language_pack` types, so binding generators never need to resolve an
        external crate's types across FFI/language boundaries.
      required:
        - text
        - context_path
        - node_types
        - byte_start
        - byte_end
      properties:
        byte_end:
          type: integer
          description: Exclusive end byte offset of this chunk in the original source.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of this chunk in the original source.
          minimum: 0
        context_path:
          type: array
          items:
            type: string
          description: Hierarchical path of enclosing structural items (e.g. `["MyClass", "my_method"]`).
        node_types:
          type: array
          items:
            type: string
          description: |-
            Tree-sitter node kinds that appear at the top level of this chunk (e.g.
            `"function_definition"`, `"class_definition"`).
        text:
          type: string
          description: The raw source text of this chunk.
    CodeDataAttribute:
      type: object
      description: |-
        An XML-style attribute attached to an `Element` node.

        Populated only for `CodeDataNodeKind::Element`; always empty for `KeyValue` and
        `Sequence` nodes.
      required:
        - name
        - value
        - byte_start
        - byte_end
      properties:
        byte_end:
          type: integer
          description: Exclusive end byte offset of the `name="value"` attribute token.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of the `name="value"` attribute token.
          minimum: 0
        name:
          type: string
          description: Attribute name (e.g. `"class"`, `"href"`).
        value:
          type: string
          description: Attribute value as a raw string (quotes stripped).
    CodeDataNode:
      type: object
      description: |-
        A node in the hierarchical data tree produced by data-format extraction.

        Purpose-built payload owned by xberg — mirrors
        `tree_sitter_language_pack::DataNode` but flattens its `Span` down to plain byte
        offsets, so binding generators never need to resolve an external crate's types
        across FFI/language boundaries.
      required:
        - kind
        - byte_start
        - byte_end
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/CodeDataAttribute'
          description: |-
            Attributes on element-shape nodes (XML `STag` attributes). Empty for all
            other kinds.
        byte_end:
          type: integer
          description: Exclusive end byte offset of this node in the original source.
          minimum: 0
        byte_start:
          type: integer
          description: Inclusive start byte offset of this node in the original source.
          minimum: 0
        children:
          type: array
          items:
            $ref: '#/components/schemas/CodeDataNode'
          description: Children for nested containers and XML element bodies.
        key:
          type:
            - string
            - "null"
          description: |-
            Key, attribute name, tag name, or positional index (`"0"`, `"1"`, …).
            `None` at the document root.
        kind:
          $ref: '#/components/schemas/CodeDataNodeKind'
          description: Whether this node is a key/value pair, XML element, or sequence item.
        value:
          type:
            - string
            - "null"
          description: |-
            Leaf scalar value, if any. `None` for containers (objects, arrays, XML
            elements with child elements).
    CodeDataNodeKind:
      type: string
      description: |-
        Discriminates the shape of a `CodeDataNode`.

        Purpose-built mirror of `tree_sitter_language_pack::DataNodeKind` — kept as an
        xberg-owned type so binding generators never need to resolve the upstream crate's
        types across FFI/language boundaries.
      enum:
        - key_value
        - element
        - sequence
    CodeMetadata:
      type: object
      description: |-
        Code-format metadata: the structural chunks produced by tree-sitter parsing.

        Wrapped by `FormatMetadata.Code`. Kept as a named struct (rather than an inline
        enum-variant body) so serde can tag it under internal tagging and utoipa can emit a
        referenceable `CodeMetadata` component in the OpenAPI schema.
      required:
        - chunks
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/CodeChunkInfo'
          description: Structural code chunks (function/class/module boundaries).
        data:
          $ref: '#/components/schemas/CodeDataNode'
          description: |-
            Hierarchical key/value data tree extracted from data-format source
            (JSON, YAML, TOML, XML, CSV, etc.), when data extraction was enabled.
    ContentLayer:
      type: string
      description: |-
        Content layer classification for document nodes.

        Replaces separate body/furniture arrays with per-node granularity.
      enum:
        - body
        - header
        - footer
        - footnote
    ContributorRole:
      type: object
      description: JATS contributor with role.
      required:
        - name
      properties:
        name:
          type: string
          description: Contributor display name.
        role:
          type:
            - string
            - "null"
          description: Contributor role (e.g. `"author"`, `"editor"`).
    CreateApiKeyRequest:
      type: object
      description: Request to create a new API key.
      properties:
        expires_at:
          type:
            - string
            - "null"
          description: |-
            Optional expiration date in ISO 8601 format. Both tiers reject the key
            after this timestamp.
        name:
          type:
            - string
            - "null"
          description: |-
            Descriptive name for the API key. Optional on Pro (unnamed keys are
            allowed); Enterprise requires a non-empty name at the handler level.
        scope:
          $ref: '#/components/schemas/ApiKeyScope'
          description: |-
            What the key may do. `read` keys are refused on POST, PUT, PATCH and
            DELETE. Omitted means `write`.
    CreateApiKeyResponse:
      type: object
      description: |-
        API key details returned only on creation/regeneration. `key` contains
        the full plaintext key and is only ever returned once.
      required:
        - id
        - key
        - key_prefix
        - scope
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        expires_at:
          type:
            - string
            - "null"
          description: |-
            Optional expiration date in ISO 8601 format. Both tiers reject the key
            after this timestamp.
        id:
          type: string
          format: uuid
          description: Unique API key identifier.
        key:
          type: string
          description: Full API key. Only returned once, on creation/regeneration.
        key_prefix:
          type: string
          description: Key prefix for identification.
        kind:
          type:
            - string
            - "null"
          description: |-
            Key kind (e.g. `"project"`, `"sandbox"`). Pro-only today; Enterprise
            always issues `"project"` keys from this endpoint but does not surface
            the kind on the wire.
        name:
          type:
            - string
            - "null"
          description: User-provided name for the key, when set.
        scope:
          $ref: '#/components/schemas/ApiKeyScope'
          description: 'What the key may do: `read` or `write`.'
    CreateIntegrationRequest:
      type: object
      description: |-
        Request to create a new integration.

        See the module-level doc for the unreconciled credential-submission
        drift between Enterprise (`provider` + `client_id` + `client_secret`) and
        Pro (`credentials` JSON blob).
      required:
        - name
        - kind
        - auth_type
      properties:
        auth_type:
          type: string
          description: 'Authentication type: `"none"`, `"static"`, `"api_key"`, `"oauth2"`, or `"oidc"`.'
        client_id:
          type:
            - string
            - "null"
          description: Client ID or username. Enterprise-only submission shape.
        client_secret:
          type:
            - string
            - "null"
          description: Client secret or password. Enterprise-only submission shape.
        config:
          type: object
          description: |-
            Additional configuration as a JSON object. Object stores require
            `bucket`; S3 also requires `region`; all accept an optional `root`.
        credentials:
          type: object
          description: |-
            Free-form BYO credentials object. Static object stores accept:
            S3 `access_key_id`/`secret_access_key` (optional `session_token`), GCS
            `credential` (service-account JSON string), and Azure Blob
            `account_name`/`account_key`.
        kind:
          type: string
          description: Integration kind (e.g. `"stripe"`, `"hubspot"`, `"google_drive"`).
        name:
          type: string
          description: Integration name (user-friendly identifier).
        provider:
          type:
            - string
            - "null"
          description: |-
            Provider name, must match a catalog entry. Enterprise-only; Pro
            derives the provider from the created record instead of the request.
    CreateInvitationRequest:
      type: object
      description: Request to create a project invitation.
      required:
        - email
        - role
      properties:
        custom_role_id:
          type:
            - string
            - "null"
          format: uuid
          description: Legacy singular custom role. Only valid with `MEMBER`.
        custom_role_ids:
          type:
            - array
            - "null"
          items:
            type: string
            format: uuid
          description: Additive custom roles. Only valid with `MEMBER`.
        email:
          type: string
          description: Email address to invite.
        role:
          type: string
          description: 'Role to assign: `"OWNER"` or `"MEMBER"`.'
    CreateInvitationResponse:
      type: object
      description: |-
        Response returned when creating an invitation. Includes the plaintext
        token (for email delivery by the caller) and omits `status`/`created_at`,
        which are implicit (`PENDING`, "now") at creation time.
      required:
        - id
        - email
        - role
        - token
        - expires_at
      properties:
        custom_role_id:
          type:
            - string
            - "null"
          format: uuid
          description: First custom role for compatibility with singular-role clients.
        custom_role_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Additive custom roles assigned on acceptance.
        email:
          type: string
          description: Invited email address.
        expires_at:
          type: string
          description: ISO 8601 expiration timestamp.
        id:
          type: string
          description: Unique invitation identifier.
        role:
          type: string
          description: Assigned role.
        token:
          type: string
          description: |-
            Invitation token to include in the invitation email link. Only
            returned once, at creation time.
    CreateProjectRequest:
      type: object
      description: |-
        Request to create a new project.

        `name` is the only field the Enterprise backend accepts — it derives a
        unique slug server-side. Pro's control plane requires the caller to supply
        `slug` directly and additionally accepts per-project quota overrides;
        those fields are optional here and are Pro-only until Enterprise grows the
        same override capability.
      required:
        - name
      properties:
        max_file_size_mb:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Maximum upload file size (MB) override. Pro-only; Enterprise applies a
            service-wide default instead.
        max_monthly_jobs:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Monthly job quota override. Pro-only; Enterprise applies a
            service-wide default instead.
        max_monthly_pages:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Monthly page quota override. Pro-only; Enterprise applies a
            service-wide default instead.
        name:
          type: string
          description: Project name.
        slug:
          type:
            - string
            - "null"
          description: |-
            URL-safe project slug. Enterprise-only omission: Enterprise derives a
            unique slug from `name` server-side and rejects this field being
            meaningfully used; Pro requires the caller to supply it.
    CreateProjectRoleRequest:
      type: object
      required:
        - name
        - permissions
      properties:
        description:
          type:
            - string
            - "null"
        name:
          type: string
        permissions:
          type: array
          items:
            $ref: '#/components/schemas/ProjectPermission'
    CreateScimConnectionRequest:
      type: object
      required:
        - display_name
      properties:
        display_name:
          type: string
    CreateScimConnectionResponse:
      type: object
      required:
        - connection
        - client_secret
        - token_endpoint
        - base_url
      properties:
        base_url:
          type: string
        client_secret:
          type: string
        connection:
          $ref: '#/components/schemas/ScimConnectionResponse'
        token_endpoint:
          type: string
    CreateWebhookRequest:
      type: object
      description: Request to create a new webhook.
      required:
        - name
        - url
        - events
      properties:
        events:
          type: array
          items:
            type: string
          description: Event types to subscribe to.
        name:
          type: string
          description: Webhook display name.
        secret:
          type:
            - string
            - "null"
          description: Optional HMAC secret. Generated server-side when not provided.
        url:
          type: string
          description: Webhook delivery URL (must use HTTPS).
    CreateWebhookResponse:
      allOf:
        - $ref: '#/components/schemas/WebhookResponse'
          description: Webhook configuration, flattened into the creation response.
        - type: object
          required:
            - secret
          properties:
            secret:
              type: string
              description: Signing secret disclosed only in the creation response, including when supplied by the caller.
      description: 'Newly created webhook and its signing secret. Store the secret securely: only creation returns it.'
    CsvMetadata:
      type: object
      description: CSV/TSV file metadata.
      required:
        - row_count
        - column_count
        - has_header
      properties:
        column_count:
          type: integer
          format: int32
          description: Number of columns detected.
          minimum: 0
        column_types:
          type:
            - array
            - "null"
          items:
            type: string
          description: Inferred data type for each column (e.g. `"string"`, `"integer"`, `"float"`).
        delimiter:
          type:
            - string
            - "null"
          description: Field delimiter character (e.g. `","` or `"\t"`).
        has_header:
          type: boolean
          description: Whether the first row was treated as a header.
        row_count:
          type: integer
          format: int32
          description: Total number of data rows (excluding the header row if present).
          minimum: 0
    DailyAnalyticsEntry:
      type: object
      description: Daily analytics record
      required:
        - date
        - jobs_total
        - jobs_completed
        - jobs_failed
        - jobs_cancelled
        - total_processing_time_ms
        - total_file_size_bytes
        - total_pages_extracted
        - total_tables_extracted
        - total_images_extracted
        - file_types
      properties:
        date:
          type: string
          description: Date (ISO 8601 format)
          example: 2025-01-15
        file_types:
          description: File type breakdown (MIME type → count)
        jobs_cancelled:
          type: integer
          format: int32
          description: Cancelled jobs
        jobs_completed:
          type: integer
          format: int32
          description: Successfully completed jobs
        jobs_failed:
          type: integer
          format: int32
          description: Failed jobs
        jobs_total:
          type: integer
          format: int32
          description: Total jobs submitted
        processing_stats:
          oneOf:
            - $ref: '#/components/schemas/ProcessingStatsEntry'
              description: Processing time percentiles (if available for this date)
            - type: "null"
        total_file_size_bytes:
          type: integer
          format: int64
          description: Total file size processed in bytes
        total_images_extracted:
          type: integer
          format: int32
          description: Total images extracted
        total_pages_extracted:
          type: integer
          format: int32
          description: Total pages extracted
        total_processing_time_ms:
          type: integer
          format: int64
          description: Total processing time across all jobs in milliseconds
        total_tables_extracted:
          type: integer
          format: int32
          description: Total tables extracted
    DbfFieldInfo:
      type: object
      description: dBASE field information.
      required:
        - name
        - field_type
      properties:
        field_type:
          type: string
          description: dBASE field type character (e.g. `"C"` for character, `"N"` for numeric).
        name:
          type: string
          description: Field (column) name.
    DbfMetadata:
      type: object
      description: dBASE (DBF) file metadata.
      required:
        - record_count
        - field_count
      properties:
        field_count:
          type: integer
          description: Number of field (column) definitions.
          minimum: 0
        fields:
          type: array
          items:
            $ref: '#/components/schemas/DbfFieldInfo'
          description: Descriptor for each field in the table schema.
        record_count:
          type: integer
          description: Total number of data records in the DBF file.
          minimum: 0
    DiffLine:
      oneOf:
        - type: object
          description: Unchanged context line.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - context
            text:
              type: string
              description: Unchanged context line.
        - type: object
          description: Line added in the "after" version.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - added
            text:
              type: string
              description: Line added in the "after" version.
        - type: object
          description: Line removed from the "before" version.
          required:
            - text
            - kind
          properties:
            kind:
              type: string
              enum:
                - removed
            text:
              type: string
              description: Line removed from the "before" version.
      description: |-
        A single line in a unified-diff hunk.

        Defined here (rather than only in `crate::diff`) so `RevisionDelta` can
        reference it unconditionally, without requiring the `diff` Cargo feature.
        `crate::diff` re-exports this type verbatim.
    DjotAttributeGroup:
      type: object
      required:
        - identifier
        - attributes
      properties:
        attributes:
          type: object
          description: |-
            Element attributes in Djot.

            Represents the attributes attached to elements using {.class #id key="value"} syntax.
          properties:
            classes:
              type: array
              items:
                type: string
              description: CSS classes (.class1 .class2)
            id:
              type:
                - string
                - "null"
              description: Element ID (#identifier)
            key_values:
              type: array
              items:
                $ref: '#/components/schemas/KeyValueAttribute'
              description: Key-value pairs (key="value")
        identifier:
          type: string
    DjotContent:
      type: object
      description: |-
        Comprehensive Djot document structure with semantic preservation.

        This type captures the full richness of Djot markup, including:
        - Block-level structures (headings, lists, blockquotes, code blocks, etc.)
        - Inline formatting (emphasis, strong, highlight, subscript, superscript, etc.)
        - Attributes (classes, IDs, key-value pairs)
        - Links, images, footnotes
        - Math expressions (inline and display)
        - Tables with full structure

        Available when the `djot` feature is enabled.
      required:
        - plain_text
        - blocks
        - metadata
        - tables
        - images
        - links
        - footnotes
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/DjotAttributeGroup'
          description: Attributes mapped by element identifier (if present)
        blocks:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Structured block-level content
        footnotes:
          type: array
          items:
            $ref: '#/components/schemas/Footnote'
          description: Footnote definitions
        images:
          type: array
          items:
            $ref: '#/components/schemas/DjotImage'
          description: Extracted images with metadata
        links:
          type: array
          items:
            $ref: '#/components/schemas/DjotLink'
          description: Extracted links with URLs
        metadata:
          $ref: '#/components/schemas/Metadata'
          description: Metadata from YAML frontmatter
        plain_text:
          type: string
          description: Plain text representation for backwards compatibility
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: Extracted tables as structured data
    DjotImage:
      type: object
      description: Image element in Djot.
      required:
        - src
        - alt
      properties:
        alt:
          type: string
          description: Alternative text
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        src:
          type: string
          description: Image source URL or path
        title:
          type:
            - string
            - "null"
          description: Optional title
    DjotLink:
      type: object
      description: Link element in Djot.
      required:
        - url
        - text
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        text:
          type: string
          description: Link text content
        title:
          type:
            - string
            - "null"
          description: Optional title
        url:
          type: string
          description: Link URL
    DocumentCounts:
      type: object
      description: |-
        Cheap structural counts for an extracted document.

        Populated on every `ExtractedDocument` returned by `extract` /
        `extract_batch`, regardless of whether the heavy `pages` / `images`
        collections are materialized. A caller that only needs "how many pages /
        tables / images did this document have?" (reporting, cost estimation,
        progress, quotas) can read these without enabling per-page or per-image
        extraction.

        The page count comes from the parse (the extractor already walks the page
        tree); it does not require opting into per-page content. `pages` is `0` for
        inputs that are not page-addressable (e.g. plain text).
      required:
        - pages
        - tables
        - images
      properties:
        images:
          type: integer
          description: Images detected in the document.
          minimum: 0
        pages:
          type: integer
          description: Total pages in the source document (`0` when not page-addressable).
          minimum: 0
        tables:
          type: integer
          description: Tables detected in the document.
          minimum: 0
    DocumentNode:
      type: object
      description: |-
        A single node in the document tree.

        Each node has deterministic `id`, typed `content`, optional `parent`/`children`
        for tree structure, and metadata like page number, bounding box, and content layer.
      required:
        - content
      properties:
        annotations:
          type: array
          items:
            $ref: '#/components/schemas/TextAnnotation'
          description: |-
            Inline annotations (formatting, links) on this node's text content.

            Only meaningful for text-carrying nodes; empty for containers.
        attributes:
          type:
            - object
            - "null"
          description: |-
            Format-specific key-value attributes.

            Extensible bag for miscellaneous data without a dedicated typed field: CSS classes,
            LaTeX environment names, Excel cell formulas, slide layout names, etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box in document coordinates.
        children:
          type: array
          items:
            $ref: '#/components/schemas/NodeIndex'
          description: Child node indices in reading order.
        content:
          $ref: '#/components/schemas/NodeContent'
          description: Node content — tagged enum, type-specific data only.
        content_layer:
          $ref: '#/components/schemas/ContentLayer'
          description: |-
            Content layer classification.

            Always serialised — Kotlin-Android (and any other typed binding) treats
            the field as non-nullable, so omitting it from the JSON wire would
            break consumer deserialisation.  `#[serde(default)]` covers the
            missing-field case on inbound JSON.
        id:
          type: string
          description: |-
            Deterministic identifier (hash of node type + text + page + position).

            Stable and unique within a single extraction response: every internal
            construction path threads the node's position (its index in
            `DocumentStructure::nodes`) into the hash, so identical
            `(node_type, text, page)` tuples at different positions never collide.
            Always serialised — `ChunkMetadata::node_ids` references it to join
            chunks back to the nodes they were derived from.
            `#[serde(default)]` covers the missing-field case on inbound JSON
            (e.g. documents serialised before this field existed).
        page:
          type:
            - integer
            - "null"
          format: int32
          description: Page number where this node starts (1-indexed).
          minimum: 0
        page_end:
          type:
            - integer
            - "null"
          format: int32
          description: Page number where this node ends (for multi-page tables/sections).
          minimum: 0
        parent:
          $ref: '#/components/schemas/NodeIndex'
          description: Parent node index (`None` = root-level node).
    DocumentRelationship:
      type: object
      description: A resolved relationship between two nodes in the document tree.
      required:
        - source
        - target
        - kind
      properties:
        kind:
          $ref: '#/components/schemas/RelationshipKind'
          description: Semantic kind of the relationship.
        source:
          $ref: '#/components/schemas/NodeIndex'
          description: Source node index (the referencing node).
        target:
          $ref: '#/components/schemas/NodeIndex'
          description: Target node index (the referenced node).
    DocumentResponse:
      type: object
      description: Single document's metadata, as reported by a connected integration.
      required:
        - id
        - name
        - mime_type
      properties:
        id:
          type: string
          description: |-
            Source-specific stable document identifier (opaque; format depends on
            the connected integration, e.g. a Google Drive file id).
        mime_type:
          type: string
          description: MIME type of the document.
        modified_at:
          type:
            - string
            - "null"
          description: Last-modified timestamp (RFC 3339), when reported by the source.
        name:
          type: string
          description: Human-readable document name.
        size_bytes:
          type:
            - integer
            - "null"
          format: int64
          description: Size in bytes, when reported by the source.
          minimum: 0
    DocumentRevision:
      type: object
      description: |-
        A single tracked change embedded in a document.

        Populated by per-format extractors that understand change-tracking metadata
        (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`, …). Every
        extractor defaults to `ExtractedDocument.revisions = None` until a
        format-specific implementation is added.
      required:
        - revision_id
        - kind
        - delta
      properties:
        anchor:
          oneOf:
            - $ref: '#/components/schemas/RevisionAnchor'
              description: |-
                Best-effort document location for this revision.

                Resolution is format-dependent and may be `None` when the location
                cannot be determined (e.g. changes inside table cells before
                table-cell anchor support is added).
            - type: "null"
        author:
          type:
            - string
            - "null"
          description: Display name of the author who made this change, when available.
        delta:
          $ref: '#/components/schemas/RevisionDelta'
          description: The content changes that make up this revision.
        kind:
          $ref: '#/components/schemas/RevisionKind'
          description: Semantic kind of this revision.
        revision_id:
          type: string
          description: |-
            Format-specific revision identifier.

            For DOCX this is the `w:id` attribute value on the change element
            (e.g. `"42"`). When the attribute is absent a synthetic fallback is
            generated (`"docx-ins-0"`, `"docx-del-3"`, …).
        timestamp:
          type:
            - string
            - "null"
          description: |-
            ISO-8601 timestamp of the change, when available.

            Stored as a plain string so this type remains FFI-friendly and
            unconditionally available without the `chrono` optional dep.
            DOCX populates this from the `w:date` attribute (e.g.
            `"2024-03-15T10:30:00Z"`).
    DocumentStructure:
      type: object
      description: |-
        Top-level structured document representation.

        A flat array of nodes with index-based parent/child references forming a tree.
        Root-level nodes have `parent: None`. Use `body_roots()` and `furniture_roots()`
        to iterate over top-level content by layer.

        # Validation

        Call `validate()` after construction to verify all node indices are in bounds
        and parent-child relationships are bidirectionally consistent.
      required:
        - nodes
      properties:
        node_types:
          type: array
          items:
            type: string
          description: |-
            Sorted, deduplicated list of node type names present in this document.

            Each value is the snake_case `node_type` tag of the corresponding
            `NodeContent` variant (e.g. `"paragraph"`, `"heading"`, `"table"`, …).

            Computed from `nodes` via node-type derivation.
            Empty until that method is called (internal construction paths call it
            at the end of derivation).
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/DocumentNode'
          description: All nodes in document/reading order.
        relationships:
          type: array
          items:
            $ref: '#/components/schemas/DocumentRelationship'
          description: |-
            Resolved relationships between nodes (footnote refs, citations, anchor links, etc.).

            Populated during derivation from the internal document representation.
            Empty when no relationships are detected.
        source_format:
          type:
            - string
            - "null"
          description: |-
            Origin format identifier (e.g. "docx", "pptx", "html", "pdf").

            Allows renderers to apply format-aware heuristics when converting
            the document tree to output formats.
    DocumentSummary:
      type: object
      description: Summary of an extracted document.
      required:
        - text
        - strategy
      properties:
        strategy:
          $ref: '#/components/schemas/SummaryStrategy'
          description: Strategy that produced this summary.
        text:
          type: string
          description: Summary text (plain prose).
        token_count:
          type:
            - integer
            - "null"
          format: int32
          description: Approximate token count of the summary, when known.
          minimum: 0
    DocxMetadata:
      type: object
      description: |-
        Word document metadata.

        Extracted from DOCX files using shared Office Open XML metadata extraction.
        Integrates with `office_metadata` module for core/app/custom properties.
      properties:
        app_properties:
          type:
            - object
            - "null"
          description: |-
            Application properties from docProps/app.xml (Word-specific statistics)

            Contains word count, page count, paragraph count, editing time, etc.
            DOCX-specific variant of Office application properties.
        core_properties:
          type:
            - object
            - "null"
          description: |-
            Core properties from docProps/core.xml (Dublin Core metadata)

            Contains title, creator, subject, keywords, dates, etc.
            Shared format across DOCX/PPTX/XLSX documents.
        custom_properties:
          type:
            - object
            - "null"
          description: |-
            Custom properties from docProps/custom.xml (user-defined properties)

            Contains key-value pairs defined by users or applications.
            Values can be strings, numbers, booleans, or dates.
          additionalProperties: {}
          propertyNames:
            type: string
    Element:
      type: object
      description: |-
        Semantic element extracted from document.

        Represents a logical unit of content with semantic classification,
        unique identifier, and metadata for tracking origin and position.
      required:
        - element_type
        - text
        - metadata
      properties:
        element_id:
          type: string
          description: |-
            Deterministic element identifier. Empty only when deserializing legacy payloads
            that predate this field's wire representation.
        element_type:
          $ref: '#/components/schemas/ElementType'
          description: Semantic type of this element
        metadata:
          $ref: '#/components/schemas/ElementMetadata'
          description: Metadata about the element
        text:
          type: string
          description: Text content of the element
    ElementMetadata:
      type: object
      description: Metadata for a semantic element.
      required:
        - additional
      properties:
        additional:
          type: object
          description: Additional custom metadata
          additionalProperties:
            type: string
          propertyNames:
            type: string
        coordinates:
          oneOf:
            - $ref: '#/components/schemas/BoundingBox'
              description: Bounding box coordinates if available
            - type: "null"
        element_index:
          type:
            - integer
            - "null"
          description: Position index in the element sequence
          minimum: 0
        filename:
          type:
            - string
            - "null"
          description: Source filename or document name
        page_number:
          type:
            - integer
            - "null"
          format: int32
          description: Page number (1-indexed)
          minimum: 0
    ElementType:
      type: string
      description: |-
        Semantic element type classification.

        Categorizes text content into semantic units for downstream processing.
        Supports the element types commonly found in Unstructured documents.
      enum:
        - title
        - narrative_text
        - heading
        - list_item
        - table
        - image
        - page_break
        - code_block
        - formula
        - block_quote
        - footer
        - header
    EmailMetadata:
      type: object
      description: |-
        Email metadata extracted from .eml and .msg files.

        Includes sender/recipient information, message ID, and attachment list.
      required:
        - to_emails
        - cc_emails
        - bcc_emails
        - attachments
      properties:
        attachments:
          type: array
          items:
            type: string
          description: List of attachment filenames
        bcc_emails:
          type: array
          items:
            type: string
          description: BCC recipients
        cc_emails:
          type: array
          items:
            type: string
          description: CC recipients
        from_email:
          type:
            - string
            - "null"
          description: Sender's email address
        from_name:
          type:
            - string
            - "null"
          description: Sender's display name
        message_id:
          type:
            - string
            - "null"
          description: Message-ID header value
        to_emails:
          type: array
          items:
            type: string
          description: Primary recipients
    Entity:
      type: object
      description: A single named entity detected in the extracted text.
      required:
        - category
        - text
        - start
        - end
      properties:
        category:
          $ref: '#/components/schemas/EntityCategory'
          description: Canonical category the entity belongs to (PERSON, ORG, LOCATION, etc.).
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Backend-reported confidence in `[0.0, 1.0]`. `None` when the backend does not
            expose confidence scores.
        end:
          type: integer
          format: int32
          description: Byte-offset span in `ExtractedDocument::content` where the mention ends (exclusive).
          minimum: 0
        start:
          type: integer
          format: int32
          description: Byte-offset span in `ExtractedDocument::content` where the mention starts.
          minimum: 0
        text:
          type: string
          description: Raw mention text exactly as it appeared in the source.
    EntityCategory:
      oneOf:
        - type: string
          description: A person's name.
          enum:
            - person
        - type: string
          description: A company, institution, or organisation name.
          enum:
            - organization
        - type: string
          description: A geographic location (city, country, address).
          enum:
            - location
        - type: string
          description: A calendar date.
          enum:
            - date
        - type: string
          description: A time of day or duration.
          enum:
            - time
        - type: string
          description: A monetary amount with optional currency.
          enum:
            - money
        - type: string
          description: A percentage value.
          enum:
            - percent
        - type: string
          description: An email address.
          enum:
            - email
        - type: string
          description: A phone number.
          enum:
            - phone
        - type: string
          description: A URL or URI.
          enum:
            - url
        - type: object
          description: A caller-supplied custom category label.
          required:
            - custom
          properties:
            custom:
              type: string
              description: A caller-supplied custom category label.
      description: |-
        Standard entity categories produced by built-in NER backends.

        The `Custom(String)` variant lets caller-supplied categories (e.g. LLM
        schemas) flow through without losing fidelity to the consumer.
    EpubMetadata:
      type: object
      description: EPUB metadata (Dublin Core extensions).
      properties:
        cover_image:
          type:
            - string
            - "null"
          description: Path or identifier of the cover image within the EPUB container.
        coverage:
          type:
            - string
            - "null"
          description: Dublin Core `coverage` field (geographic or temporal scope).
        dc_format:
          type:
            - string
            - "null"
          description: Dublin Core `format` field (media type of the resource).
        dc_type:
          type:
            - string
            - "null"
          description: Dublin Core `type` field (nature or genre of the resource).
        relation:
          type:
            - string
            - "null"
          description: Dublin Core `relation` field (related resource identifier).
        source:
          type:
            - string
            - "null"
          description: Dublin Core `source` field (origin resource identifier).
    ErrorBody:
      type: object
      description: Error body containing code, message, and optional details.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Stable string error code (e.g., "rag.vector.dim_mismatch", "not_found")
          example: rag.vector.dim_mismatch
        details:
          type: object
          description: Optional detailed error information (context-specific)
        message:
          type: string
          description: Human-readable error message
          example: Caller-supplied embedding does not match collection dimension
    ErrorDetail:
      type: object
      description: Error detail for failed requests
      required:
        - timestamp
        - method
        - path
        - status_code
      properties:
        error_message:
          type:
            - string
            - "null"
          description: Error message
          example: Invalid file format
        method:
          type: string
          description: HTTP method
          example: POST
        path:
          type: string
          description: Request path
          example: /v1/projects/550e8400-e29b-41d4-a716-446655440000/jobs
        status_code:
          type: integer
          format: int32
          description: HTTP status code
          example: 400
        timestamp:
          type: string
          description: ISO 8601 timestamp of the error
          example: 2025-01-15T14:30:00Z
    ErrorMetadata:
      type: object
      description: Error metadata (for batch operations).
      required:
        - error_type
        - message
      properties:
        error_type:
          type: string
          description: Machine-readable error type identifier (e.g. "UnsupportedFormat").
        message:
          type: string
          description: Human-readable error description.
    ErrorResponse:
      type: object
      description: |-
        Canonical error response body.

        Wraps the nested error envelope required by all API error paths.
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
          description: The nested error body with code, message, and optional details.
    ExcelMetadata:
      type: object
      description: |-
        Excel/spreadsheet format metadata.

        Identifies the document as a spreadsheet source via the `FormatMetadata::Excel`
        discriminant. Sheet count and sheet names are stored inside this struct.
      properties:
        sheet_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of sheets in the workbook.
          minimum: 0
        sheet_names:
          type:
            - array
            - "null"
          items:
            type: string
          description: Names of all sheets in the workbook.
    ExtractedDocument:
      type: object
      description: |-
        Document extracted by the core extraction pipeline.

        `extract` and `extract_batch` return an `ExtractionResult` envelope whose
        `results` field contains these per-document payloads.
      required:
        - content
        - mime_type
        - metadata
        - tables
      properties:
        annotations:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PdfAnnotation'
          description: |-
            PDF annotations extracted from the document.

            When annotation extraction is enabled via `PdfConfig::extract_annotations`,
            this field contains text notes, highlights, links, stamps, and other
            annotations found in PDF documents.
        children:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ArchiveEntry'
          description: |-
            Nested extraction results from archive contents.

            When extracting archives, each processable file inside produces its own
            full extraction result. Set to `None` for non-archive formats.
            Use `max_archive_depth` in config to control recursion depth.
        chunks:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Chunk'
          description: |-
            Text chunks when chunking is enabled.

            When chunking configuration is provided, the content is split into
            overlapping chunks for efficient processing. Each chunk contains the text,
            optional embeddings (if enabled), and metadata about its position.
        code_intelligence:
          description: |-
            Code intelligence results from tree-sitter analysis.

            Populated when extracting source code files with the `tree-sitter` feature.
            Contains metrics, structural analysis, imports/exports, comments,
            docstrings, symbols, diagnostics, and optionally chunked code segments.

            Stored as an opaque JSON value so that all language bindings (Go, Java,
            C#, …) can deserialize it as a raw JSON object rather than a typed struct.
            The underlying type is `tree_sitter_language_pack::ProcessResult`.
        content:
          type: string
          description: Plain-text representation of the extracted document content.
        counts:
          $ref: '#/components/schemas/DocumentCounts'
          description: |-
            Cheap structural counts (pages, tables, images).

            Always populated by the extraction pipeline, even when the `pages` /
            `images` collections are `None`. See `DocumentCounts`.
        detected_language_confidences:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LanguageConfidence'
          description: |-
            Structured per-language detection results: confidence, document share, script,
            and reliability, alongside the ISO-code-only `detected_languages` (#261).

            One entry per language in `detected_languages`, in the same order. `None` under
            the same conditions as `detected_languages`: detection disabled, empty input
            text, or no language met the configured `min_confidence`.
        detected_languages:
          type:
            - array
            - "null"
          items:
            type: string
          description: ISO 639-1 language codes detected in the document content.
        djot_content:
          $ref: '#/components/schemas/DjotContent'
          description: |-
            Rich Djot content structure (when extracting Djot documents).

            When extracting Djot documents with structured extraction enabled,
            this field contains the full semantic structure including:
            - Block-level elements with nesting
            - Inline formatting with attributes
            - Links, images, footnotes
            - Math expressions
            - Complete attribute information

            The `content` field still contains plain text for backward compatibility.

            Always `None` for non-Djot documents.
        document:
          $ref: '#/components/schemas/DocumentStructure'
          description: |-
            Structured document tree (when document structure extraction is enabled).

            When `include_document_structure` is true in `ExtractionConfig`, this field
            contains the full hierarchical representation of the document including:
            - Heading-driven section nesting
            - Table grids with cell-level metadata
            - Content layer classification (body, header, footer, footnote)
            - Inline text annotations (formatting, links)
            - Bounding boxes and page numbers

            Independent of `result_format` — can be combined with Unified or ElementBased.
        elements:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Element'
          description: |-
            Semantic elements when element-based result format is enabled.

            When result_format is set to ElementBased, this field contains semantic
            elements with type classification, unique identifiers, and metadata for
            Unstructured-compatible element-based processing.
        entities:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Entity'
          description: |-
            Named entities detected in `content` by the NER post-processor.

            `None` when no NER backend is configured. Populated by the `xberg-gliner`
            ONNX backend or the LLM-driven backend (see `crates/xberg/src/text/ner/`).
        extracted_keywords:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/Keyword'
          description: |-
            Extracted keywords when keyword extraction is enabled.

            When keyword extraction (RAKE or YAKE) is configured, this field contains
            the extracted keywords with scores, algorithm info, and position data.
            Previously stored in `metadata.additional["keywords"]`.
        extraction_confidence:
          $ref: '#/components/schemas/ExtractionConfidence'
          description: |-
            Confidence score computed by the heuristics pipeline.

            Populated when the `heuristics` feature is enabled and confidence
            scoring has been performed.  Combines text-coverage, OCR aggregate
            confidence, and schema-compliance into a single `[0, 1]` value.

            `None` when confidence scoring is not configured or the feature is
            absent.
        extraction_method:
          $ref: '#/components/schemas/ExtractionMethod'
          description: |-
            Extraction strategy used to produce the returned text.

            Populated when the extractor can reliably distinguish native text extraction,
            OCR-only extraction, or mixed native/OCR output.
        form_fields:
          type: array
          items:
            $ref: '#/components/schemas/PdfFormField'
          description: |-
            Form fields extracted from a PDF's AcroForm or XFA structure.

            Populated by the PDF extractor when `PdfConfig::extract_form_fields` is
            enabled (default) and the document is a fillable form. Empty otherwise.
        formulas:
          type: array
          items:
            $ref: '#/components/schemas/Formula'
          description: |-
            Mathematical formulas recognized in the document.

            Populated from every source that produces formulas: layout-guided OCR
            (with geometry), VLM OCR (text only), and markup extraction (DOCX,
            PPTX, ODT, EPUB, HTML, JATS, LaTeX, Markdown, and related formats,
            without geometry). Empty when the document contains no formulas.
        images:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ExtractedImage'
          description: |-
            Extracted images from the document.

            When image extraction is enabled via `ImageExtractionConfig`, this field
            contains all images found in the document with their raw data and metadata.
            Each image may optionally contain a nested `ocr_result` if OCR was performed.
        llm_usage:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LlmUsage'
          description: |-
            LLM token usage and cost data for all LLM calls made during this extraction.

            Contains one entry per LLM call. Multiple entries are produced when
            VLM OCR, structured extraction, or LLM embeddings run during
            the same extraction.

            `None` when no LLM was used.
        metadata:
          $ref: '#/components/schemas/Metadata'
          description: Document-level metadata (author, title, dates, format-specific fields).
        mime_type:
          type: string
          description: MIME type of the source document (e.g. `"application/pdf"`).
        ocr_elements:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/OcrElement'
          description: |-
            OCR elements with full spatial and confidence metadata.

            When OCR is performed with element extraction enabled, this field contains
            the structured representation of detected text including:
            - Bounding geometry (rectangles or quadrilaterals)
            - Confidence scores (detection and recognition)
            - Rotation information
            - Hierarchical relationships (Tesseract only)

            This field preserves all metadata that would otherwise be lost when
            converting to plain text or markdown output formats.

            Only populated when `OcrElementConfig.include_elements` is true.
        ocr_page_failures:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/OcrPageFailure'
          description: |-
            Pages whose OCR failed while extraction continued, in the order the failures were reported.

            A record with `recovered` set means the document still has content for that page
            from native text or from embedded images. Xberg-produced results use `None` when
            no page failed.
        page_classifications:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageClassification'
          description: |-
            Per-page classifications produced by the page-classification post-processor.

            `None` when classification is not configured.
        pages:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageContent'
          description: |-
            Per-page content when page extraction is enabled.

            When page extraction is configured, the document is split into per-page content
            with tables and images mapped to their respective pages.
        processing_warnings:
          type: array
          items:
            $ref: '#/components/schemas/ProcessingWarning'
          description: |-
            Non-fatal warnings collected during processing pipeline stages.

            Captures errors from optional pipeline features (embedding, chunking,
            language detection, output formatting) that don't prevent extraction
            but may indicate degraded or incomplete results. These warnings are
            independent of `quality_score`, which assesses only retained text.
            Previously stored as individual keys in `metadata.additional`.
        quality_score:
          type:
            - number
            - "null"
          format: double
          description: |-
            Text cleanliness/readability score from quality analysis.

            A value between 0.0 and 1.0 describing the quality of the text that was
            retained. This is not a completeness or recall score: clean text can score
            highly even when an extractor omitted or rejected other content. Inspect
            `processing_warnings` separately for known degraded or partial extraction.

            When the text came from OCR and the result carries enough recognized words to
            judge, this score is additionally capped by the mean OCR recognition confidence.
            Text that looks clean but that OCR itself had little confidence in therefore
            cannot score high. A native, non-OCR extraction is not capped.
            Previously stored in `metadata.additional["quality_score"]`.
        redaction_report:
          $ref: '#/components/schemas/RedactionReport'
          description: |-
            Audit report of redactions applied by the redaction post-processor.

            The redaction processor rewrites `content`, `formatted_content`, every
            chunk's text, and the textual fields of `entities` / `summary` / `translation` /
            `page_classifications` in place. This report describes what was found and how it
            was replaced. `None` when redaction is not configured.
        revisions:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/DocumentRevision'
          description: |-
            Tracked changes embedded in the source document.

            Populated by per-format extractors that understand change-tracking
            metadata (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`,
            …). Every extractor defaults to `None` until its format-specific
            implementation is added. Extractors that do populate this field follow
            the "accepted-changes" convention: inserted text is present in
            `content`, deleted text is absent — the revision list is the separate
            audit trail.
        structured_output:
          description: |-
            Structured extraction output from LLM-based JSON schema extraction.

            When `structured_extraction` is configured in `ExtractionConfig`, the
            extracted document content is sent to a VLM with the provided JSON schema.
            The response is parsed and stored here as a JSON value matching the schema.
        summary:
          $ref: '#/components/schemas/DocumentSummary'
          description: |-
            Summary of `content` produced by the summarisation post-processor.

            `None` when summarisation is not configured. Populated by the TextRank
            extractive backend (deterministic, no external service) or by the
            liter-llm-driven abstractive backend.
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: Tables extracted from the document, each with structured cell data.
        translation:
          $ref: '#/components/schemas/Translation'
          description: |-
            Translation of `content` produced by the translation post-processor.

            `None` when translation is not configured.
        uris:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ExtractedUri'
          description: |-
            URIs/links discovered during document extraction.

            Contains hyperlinks, image references, citations, email addresses, and
            other URI-like references found in the document. Always extracted when
            present in the source document.
    ExtractedImage:
      type: object
      description: |-
        Extracted image from a document.

        Contains raw image data, metadata, and optional nested OCR results.
        Raw bytes allow cross-language compatibility - users can convert to
        PIL.Image (Python), Sharp (Node.js), or other formats as needed.
      required:
        - data
        - format
        - image_index
      properties:
        bits_per_component:
          type:
            - integer
            - "null"
          format: int32
          description: Bits per color component (e.g., 8, 16)
          minimum: 0
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the image on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
            Only populated for PDF-extracted images when position data is available from the PDF extractor.
        caption:
          type:
            - string
            - "null"
          description: |-
            VLM-generated caption describing the image, when captioning is configured.

            Populated by the captioning post-processor
            (`crates/xberg/src/plugins/processor/builtin/captioning.rs`), which routes
            each image through `crate::llm::region_extractor::extract_region_with_vlm` in
            caption mode. `None` when captioning is disabled or the VLM declined to caption.
        cluster_id:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Identifier shared across images that form a single logical figure
            (e.g. all raster tiles of one technical drawing). `None` for singletons.
          minimum: 0
        colorspace:
          type:
            - string
            - "null"
          description: Colorspace information (e.g., "RGB", "CMYK", "Gray")
        data:
          type: array
          items:
            type: integer
            format: binary
            minimum: 0
          description: |-
            Raw image data (PNG, JPEG, WebP, etc. bytes).
            Uses `bytes::Bytes` for cheap cloning of large buffers.
        data_base64:
          type:
            - string
            - "null"
          description: |-
            Base64-encoded copy of `data`; populated when `ImageExtractionConfig::include_data_base64`
            is `true`. Omitted from JSON by default; use instead of `data` in JSON-only clients.
        description:
          type:
            - string
            - "null"
          description: Optional description of the image
        format:
          type: string
          description: |-
            Image format (e.g., "jpeg", "png", "webp")
            Uses Cow<'static, str> to avoid allocation for static literals.
        height:
          type:
            - integer
            - "null"
          format: int32
          description: Image height in pixels
          minimum: 0
        image_index:
          type: integer
          format: int32
          description: Zero-indexed position of this image in the document/page
          minimum: 0
        image_kind:
          $ref: '#/components/schemas/ImageKind'
          description: |-
            Heuristic classification of what this image likely depicts.
            `None` if classification was disabled or inconclusive.
        is_mask:
          type: boolean
          description: Whether this image is a mask image
        kind_confidence:
          type:
            - number
            - "null"
          format: float
          description: Confidence score for `image_kind`, in the range 0.0 to 1.0.
        ocr_result:
          $ref: '#/components/schemas/ExtractedDocument'
          description: |-
            Nested OCR extraction result (if image was OCRed)

            When OCR is performed on this image, the result is embedded here
            rather than in a separate collection, making the relationship explicit.
        page_number:
          type:
            - integer
            - "null"
          format: int32
          description: Page/slide number where image was found (1-indexed)
          minimum: 0
        qr_codes:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/QrCode'
          description: |-
            QR codes decoded from this image, when QR detection is enabled.

            Populated by the QR post-processor (`crates/xberg/src/extractors/qr.rs`) via
            the pure-Rust `rqrr` decoder. `None` when QR detection is disabled; an empty
            `Some(vec![])` when detection ran but found nothing.
        source_path:
          type:
            - string
            - "null"
          description: |-
            Original source path of the image within the document archive (e.g., "media/image1.png" in DOCX).
            Used for rendering image references when the binary data is not extracted.
        width:
          type:
            - integer
            - "null"
          format: int32
          description: Image width in pixels
          minimum: 0
    ExtractedUri:
      type: object
      description: |-
        A URI extracted from a document.

        Represents any link, reference, or resource pointer found during extraction.
        The `kind` field classifies the URI semantically, while `label` carries
        optional human-readable display text.
      required:
        - url
        - kind
      properties:
        kind:
          $ref: '#/components/schemas/UriKind'
          description: Semantic classification of the URI.
        label:
          type:
            - string
            - "null"
          description: Optional display text / label for the link.
        page:
          type:
            - integer
            - "null"
          format: int32
          description: Optional page number where the URI was found (1-indexed).
          minimum: 0
        url:
          type: string
          description: The URL or path string.
    ExtractionConfidence:
      type: object
      description: |-
        Combined confidence on `[0, 1]`.

        When OCR did not run, the `ocr_aggregate` weight folds into `text_coverage`
        so the weighted sum still totals 1.0.
      required:
        - text_coverage
        - schema_compliance
        - combined
      properties:
        combined:
          type: number
          format: float
          description: Weighted blend in `[0, 1]`.  The value compared against the fallback threshold.
        ocr_aggregate:
          type:
            - number
            - "null"
          format: float
          description: |-
            OCR recognition confidence, word-count-weighted across every recognized word, when OCR
            ran; `None` when it did not.
        schema_compliance:
          $ref: '#/components/schemas/SchemaCompliance'
          description: Whether the merged output validates against the preset schema.
        text_coverage:
          type: number
          format: float
          description: Fraction of pages with a usable text layer.
    ExtractionMethod:
      type: string
      description: How the extracted text was produced.
      enum:
        - native
        - ocr
        - mixed
    FictionBookMetadata:
      type: object
      description: FictionBook (FB2) metadata.
      properties:
        annotation:
          type:
            - string
            - "null"
          description: Short annotation / summary from the FB2 `<annotation>` element.
        genres:
          type: array
          items:
            type: string
          description: Genre tags as declared in the FB2 `<genre>` elements.
        sequences:
          type: array
          items:
            type: string
          description: Book series (sequence) names, if any.
    Footnote:
      type: object
      description: Footnote in Djot.
      required:
        - label
        - content
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Footnote content blocks
        label:
          type: string
          description: Footnote label
    FormFieldType:
      type: string
      description: |-
        Kind of a PDF form field.

        Mirrors `xberg_native_pdf`'s widget field taxonomy without leaking the upstream
        type across the binding surface.
      enum:
        - text
        - checkbox
        - radio
        - choice
        - signature
        - button
        - unknown
    FormatMetadata:
      oneOf:
        - $ref: '#/components/schemas/PdfMetadata'
        - $ref: '#/components/schemas/DocxMetadata'
        - $ref: '#/components/schemas/ExcelMetadata'
        - $ref: '#/components/schemas/EmailMetadata'
        - $ref: '#/components/schemas/PptxMetadata'
        - $ref: '#/components/schemas/ArchiveMetadata'
        - $ref: '#/components/schemas/ImageMetadata'
        - $ref: '#/components/schemas/XmlMetadata'
        - $ref: '#/components/schemas/TextMetadata'
        - $ref: '#/components/schemas/HtmlMetadata'
        - $ref: '#/components/schemas/OcrMetadata'
        - $ref: '#/components/schemas/CsvMetadata'
        - $ref: '#/components/schemas/BibtexMetadata'
        - $ref: '#/components/schemas/CitationMetadata'
        - $ref: '#/components/schemas/FictionBookMetadata'
        - $ref: '#/components/schemas/DbfMetadata'
        - $ref: '#/components/schemas/EpubMetadata'
        - $ref: '#/components/schemas/JatsMetadata'
        - $ref: '#/components/schemas/PstMetadata'
        - $ref: '#/components/schemas/AudioMetadata'
        - $ref: '#/components/schemas/CodeMetadata'
      description: Format-specific metadata (discriminated union). Only one format type can exist per extraction result.
      discriminator:
        propertyName: format_type
        mapping:
          archive: '#/components/schemas/ArchiveMetadata'
          audio: '#/components/schemas/AudioMetadata'
          bibtex: '#/components/schemas/BibtexMetadata'
          citation: '#/components/schemas/CitationMetadata'
          code: '#/components/schemas/CodeMetadata'
          csv: '#/components/schemas/CsvMetadata'
          dbf: '#/components/schemas/DbfMetadata'
          docx: '#/components/schemas/DocxMetadata'
          email: '#/components/schemas/EmailMetadata'
          epub: '#/components/schemas/EpubMetadata'
          excel: '#/components/schemas/ExcelMetadata'
          fiction_book: '#/components/schemas/FictionBookMetadata'
          html: '#/components/schemas/HtmlMetadata'
          image: '#/components/schemas/ImageMetadata'
          jats: '#/components/schemas/JatsMetadata'
          ocr: '#/components/schemas/OcrMetadata'
          pdf: '#/components/schemas/PdfMetadata'
          pptx: '#/components/schemas/PptxMetadata'
          pst: '#/components/schemas/PstMetadata'
          text: '#/components/schemas/TextMetadata'
          xml: '#/components/schemas/XmlMetadata'
    FormattedBlock:
      type: object
      description: |-
        Block-level element in a Djot document.

        Represents structural elements like headings, paragraphs, lists, code blocks, etc.
      required:
        - block_type
        - inline_content
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes (classes, IDs, key-value pairs)
        block_type:
          $ref: '#/components/schemas/BlockType'
          description: Type of block element
        children:
          type: array
          items:
            $ref: '#/components/schemas/FormattedBlock'
          description: Nested blocks for containers (blockquotes, list items, divs)
        code:
          type:
            - string
            - "null"
          description: Raw code content for code blocks
        inline_content:
          type: array
          items:
            $ref: '#/components/schemas/InlineElement'
          description: Inline content within the block
        language:
          type:
            - string
            - "null"
          description: Language identifier for code blocks
        level:
          type:
            - integer
            - "null"
          description: Heading level (1-6) for headings, or nesting level for lists
          minimum: 0
    Formula:
      type: object
      description: |-
        A mathematical formula extracted from a document.

        Three kinds of sources populate this type. Layout-guided OCR detects
        formula regions and recognizes them; those formulas carry a `bbox` and a
        `page`. VLM OCR recognizes formulas in transcribed text without layout, so
        its formulas carry no geometry. Markup extraction (DOCX, PPTX, ODT, EPUB,
        HTML, JATS, LaTeX, Markdown, and related formats) converts embedded math
        to LaTeX, also without geometry.
      required:
        - latex
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the formula region on its page. `None` for markup sources.

            PDF OCR sources report PDF point coordinates with the origin at the
            bottom-left of the page, comparable to native PDF geometry. Image
            sources, and PDF pages whose geometry is unavailable, report pixels of
            the image the OCR backend saw. The C FFI reports an absent bbox as a
            null pointer.
        latex:
          type: string
          description: |-
            LaTeX source of the formula, without surrounding `$$` delimiters.

            Markup converters and formula OCR produce real LaTeX. The native PDF
            layout path stores the plain text of a detected formula region, which
            keeps the original Unicode math characters instead of LaTeX commands.
            To render the formula in Markdown or other formats, wrap it in `$$..$$`.
        page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            1-indexed page number the formula appears on. `None` when the source
            format has no page concept. The C FFI reports an absent page as `0`.
          minimum: 0
    GridCell:
      type: object
      description: Individual grid cell with position and span metadata.
      required:
        - content
        - row
        - col
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box for this cell (if available).
        col:
          type: integer
          format: int32
          description: Zero-indexed column position.
          minimum: 0
        col_span:
          type: integer
          format: int32
          description: Number of columns this cell spans.
          minimum: 0
        content:
          type: string
          description: Cell text content.
        heading_level:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            Outline level (1-6) of the heading style this cell's text carries, when it has one.

            A DOCX banner row -- row 0, one cell spanning the grid, styled `Heading1`..`Heading6` --
            is what Word's navigation pane and a `TOC` field treat as the document's outline, but as
            a table cell it reached consumers as anonymous text (GH#1587). `content` is deliberately
            left as the bare cell text: prefixing it with `#` would put a markdown heading inside a
            table cell, which is invalid where it lands and changes text every existing consumer
            already reads. This field is the signal instead, so a caller can decide for itself
            whether a `heading 2` in a banner row is a section title or a column label.
          minimum: 0
        is_header:
          type: boolean
          description: Whether this is a header cell.
        row:
          type: integer
          format: int32
          description: Zero-indexed row position.
          minimum: 0
        row_span:
          type: integer
          format: int32
          description: Number of rows this cell spans.
          minimum: 0
        style_name:
          type:
            - string
            - "null"
          description: |-
            Human-readable name of the paragraph style applied to this cell's text (`heading 2`).

            Carries the style even when it resolves to no outline level, so a caller can key on a
            named style this crate does not map to a heading. See `GridCell.heading_level`.
    HeaderMetadata:
      type: object
      description: Header/heading element metadata.
      required:
        - level
        - text
        - depth
        - html_offset
      properties:
        depth:
          type: integer
          format: int32
          description: Document tree depth at the header element
          minimum: 0
        html_offset:
          type: integer
          format: int32
          description: Byte offset in original HTML document
          minimum: 0
        id:
          type:
            - string
            - "null"
          description: HTML id attribute if present
        level:
          type: integer
          format: int32
          description: 'Header level: 1 (h1) through 6 (h6)'
          minimum: 0
        text:
          type: string
          description: Normalized text content of the header
    HeadingContext:
      type: object
      description: |-
        Heading context for a chunk within a Markdown document.

        Contains the heading hierarchy from document root to this chunk's section.
      required:
        - headings
      properties:
        headings:
          type: array
          items:
            $ref: '#/components/schemas/HeadingLevel'
          description: |-
            The heading hierarchy from document root to this chunk's section.
            Index 0 is the outermost (h1), last element is the most specific.
    HeadingLevel:
      type: object
      description: A single heading in the hierarchy.
      required:
        - level
        - text
      properties:
        level:
          type: integer
          format: int32
          description: Heading depth (1 = h1, 2 = h2, etc.)
          minimum: 0
        text:
          type: string
          description: The text content of the heading.
    HealthResponse:
      type: object
      description: Backend liveness and deployment capabilities used by the dashboard.
      required:
        - status
        - tier
      properties:
        auto_tune:
          oneOf:
            - type: object
              description: Whether Enterprise mounts a feature API.
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                  description: Instance availability, not a grant of project access or billing entitlement.
            - type: "null"
          description: Enterprise AutoTune availability; project entitlement is checked on submission.
        rag:
          oneOf:
            - type: object
              description: Whether Enterprise mounts a feature API.
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
                  description: Instance availability, not a grant of project access or billing entitlement.
            - type: "null"
          description: Enterprise RAG availability; project configuration is checked on each request.
        status:
          type: string
          description: Process liveness status.
        tier:
          type: string
          description: Deployment tier.
    HierarchicalBlock:
      type: object
      required:
        - text
        - font_size
        - level
      properties:
        bbox:
          $ref: '#/components/schemas/HierarchicalBoundingBox'
          description: |-
            Bounding box information for the block

            Contains left, top, right, and bottom coordinates in PDF units.
        font_size:
          type: number
          format: float
          description: The font size of the text in this block
        level:
          type: string
          description: |-
            The hierarchy level of this block (H1-H6 or Body)

            Levels correspond to HTML heading tags:
            - "h1": Top-level heading
            - "h2": Secondary heading
            - "h3": Tertiary heading
            - "h4": Quaternary heading
            - "h5": Quinary heading
            - "h6": Senary heading
            - "body": Body text (no heading level)
        text:
          type: string
          description: The text content of this block
    HierarchicalBoundingBox:
      type: object
      required:
        - left
        - top
        - right
        - bottom
      properties:
        bottom:
          type: number
        left:
          type: number
        right:
          type: number
        top:
          type: number
    HtmlMetadata:
      type: object
      description: |-
        HTML metadata extracted from HTML documents.

        Includes document-level metadata, Open Graph data, Twitter Card metadata,
        and extracted structural elements (headers, links, images, structured data).
      properties:
        author:
          type:
            - string
            - "null"
          description: Document author from `<meta name="author">` tag
        base_href:
          type:
            - string
            - "null"
          description: Base URL from `<base href="">` tag for resolving relative URLs
        canonical_url:
          type:
            - string
            - "null"
          description: Canonical URL from `<link rel="canonical">` tag
        description:
          type:
            - string
            - "null"
          description: Document description from `<meta name="description">` tag
        headers:
          type: array
          items:
            $ref: '#/components/schemas/HeaderMetadata'
          description: Extracted header elements with hierarchy
        images:
          type: array
          items:
            $ref: '#/components/schemas/ImageMetadataType'
          description: Extracted images with source and dimensions
        keywords:
          type: array
          items:
            type: string
          description: Document keywords from `<meta name="keywords">` tag, split on commas
        language:
          type:
            - string
            - "null"
          description: Document language from `lang` attribute
        links:
          type: array
          items:
            $ref: '#/components/schemas/LinkMetadata'
          description: Extracted hyperlinks with type classification
        meta_tags:
          type: object
          description: |-
            Additional meta tags not covered by specific fields
            Keys are meta name/property attributes, values are content
          additionalProperties:
            type: string
          propertyNames:
            type: string
        open_graph:
          type: object
          description: |-
            Open Graph metadata (og:* properties) for social media
            Keys like "title", "description", "image", "url", etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        structured_data:
          type: array
          items:
            $ref: '#/components/schemas/StructuredData'
          description: Extracted structured data blocks
        text_direction:
          $ref: '#/components/schemas/TextDirection'
          description: Document text direction from `dir` attribute
        title:
          type:
            - string
            - "null"
          description: Document title from `<title>` tag
        twitter_card:
          type: object
          description: |-
            Twitter Card metadata (twitter:* properties)
            Keys like "card", "site", "creator", "title", "description", "image", etc.
          additionalProperties:
            type: string
          propertyNames:
            type: string
    ImageDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: integer
        width:
          type: integer
    ImageDpi:
      type: object
      required:
        - horizontal
        - vertical
      properties:
        horizontal:
          type: number
        vertical:
          type: number
    ImageKind:
      type: string
      description: Heuristic classification of what an image likely depicts.
      enum:
        - photograph
        - diagram
        - chart
        - drawing
        - text_block
        - decoration
        - logo
        - icon
        - tile_fragment
        - mask
        - page_raster
        - unknown
    ImageMetadata:
      type: object
      description: |-
        Image metadata extracted from image files.

        Includes dimensions, format, and EXIF data.
      required:
        - width
        - height
        - format
        - exif
      properties:
        exif:
          type: object
          description: EXIF metadata tags
          additionalProperties:
            type: string
          propertyNames:
            type: string
        format:
          type: string
          description: Image format (e.g., "PNG", "JPEG", "TIFF")
        height:
          type: integer
          format: int32
          description: Image height in pixels
          minimum: 0
        width:
          type: integer
          format: int32
          description: Image width in pixels
          minimum: 0
    ImageMetadataType:
      type: object
      description: Image element metadata.
      required:
        - src
        - image_type
        - attributes
      properties:
        alt:
          type:
            - string
            - "null"
          description: Alternative text from alt attribute
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Additional attributes as key-value pairs.
        dimensions:
          oneOf:
            - $ref: '#/components/schemas/ImageDimensions'
              description: Image dimensions if available.
            - type: "null"
        image_type:
          $ref: '#/components/schemas/ImageType'
          description: Image type classification
        src:
          type: string
          description: Image source (URL, data URI, or SVG content)
        title:
          type:
            - string
            - "null"
          description: Title attribute
    ImagePreprocessingMetadata:
      type: object
      required:
        - original_dimensions
        - original_dpi
        - target_dpi
        - scale_factor
        - auto_adjusted
        - final_dpi
        - resample_method
        - dimension_clamped
        - skipped_resize
      properties:
        auto_adjusted:
          type: boolean
          description: Whether DPI was auto-adjusted based on content
        calculated_dpi:
          type:
            - integer
            - "null"
          format: int32
          description: Calculated optimal DPI (if auto_adjust_dpi enabled)
        dimension_clamped:
          type: boolean
          description: Whether dimensions were clamped to max_image_dimension
        final_dpi:
          type: integer
          format: int32
          description: Final DPI after processing
        new_dimensions:
          oneOf:
            - $ref: '#/components/schemas/PixelDimensions'
              description: New dimensions after resizing (if resized).
            - type: "null"
        original_dimensions:
          $ref: '#/components/schemas/PixelDimensions'
          description: Original image dimensions in pixels.
        original_dpi:
          $ref: '#/components/schemas/ImageDpi'
          description: Original image resolution.
        resample_method:
          type: string
          description: Resampling algorithm used ("LANCZOS3", "CATMULLROM", etc.)
        resize_error:
          type:
            - string
            - "null"
          description: Error message if resize failed
        scale_factor:
          type: number
          format: double
          description: Scaling factor applied to the image
        skipped_resize:
          type: boolean
          description: Whether resize was skipped (dimensions already optimal)
        target_dpi:
          type: integer
          format: int32
          description: Target DPI from configuration
    ImageType:
      type: string
      description: Image type classification.
      enum:
        - data-uri
        - inline-svg
        - external
        - relative
    InlineElement:
      type: object
      description: |-
        Inline element within a block.

        Represents text with formatting, links, images, etc.
      required:
        - element_type
        - content
      properties:
        attributes:
          $ref: '#/components/schemas/Attributes'
          description: Element attributes
        content:
          type: string
          description: Text content
        element_type:
          $ref: '#/components/schemas/InlineType'
          description: Type of inline element
        metadata:
          type:
            - object
            - "null"
          description: Additional metadata (e.g., href for links, src/alt for images)
          additionalProperties:
            type: string
          propertyNames:
            type: string
    InlineType:
      type: string
      description: Types of inline elements in Djot.
      enum:
        - text
        - strong
        - emphasis
        - highlight
        - subscript
        - superscript
        - insert
        - delete
        - code
        - link
        - image
        - span
        - math
        - raw_inline
        - footnote_ref
        - symbol
    IntegrationResponse:
      type: object
      description: |-
        Integration details for get/list/create responses. Never includes
        secrets or credentials, only integration metadata.
      required:
        - id
        - name
        - kind
        - auth_type
        - provider
      properties:
        auth_type:
          type: string
          description: Authentication type.
        id:
          type: string
          format: uuid
          description: Unique integration identifier.
        kind:
          type: string
          description: Integration kind.
        name:
          type: string
          description: User-friendly integration name.
        project_id:
          type:
            - string
            - "null"
          description: |-
            Owning project ID. Pro-only; Enterprise omits it (already scoped by
            the request path).
        provider:
          type: string
          description: Provider name.
    IntegrationSyncResponse:
      type: object
      required:
        - enabled
        - interval_seconds
      properties:
        enabled:
          type: boolean
        interval_seconds:
          type: integer
          format: int32
    InvitationResponse:
      type: object
      description: Invitation details, as returned by list.
      required:
        - id
        - email
        - role
        - status
        - expires_at
        - created_at
      properties:
        created_at:
          type: string
          description: ISO 8601 creation timestamp.
        custom_role_id:
          type:
            - string
            - "null"
          format: uuid
          description: First custom role for compatibility with singular-role clients.
        custom_role_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Additive custom roles assigned on acceptance.
        email:
          type: string
          description: Invited email address.
        expires_at:
          type: string
          description: ISO 8601 expiration timestamp.
        id:
          type: string
          description: Unique invitation identifier.
        role:
          type: string
          description: Assigned role.
        status:
          type: string
          description: Invitation status (e.g. `"PENDING"`, `"ACCEPTED"`, `"REVOKED"`).
    JatsMetadata:
      type: object
      description: JATS (Journal Article Tag Suite) metadata.
      properties:
        contributor_roles:
          type: array
          items:
            $ref: '#/components/schemas/ContributorRole'
          description: Authors and contributors with their stated roles.
        copyright:
          type:
            - string
            - "null"
          description: Copyright statement from the article's `<permissions>` element.
        history_dates:
          type: object
          description: Publication history dates keyed by event type (e.g. `"received"`, `"accepted"`).
          additionalProperties:
            type: string
          propertyNames:
            type: string
        license:
          type:
            - string
            - "null"
          description: Open-access license URI from the article's `<license>` element.
    KeyValueAttribute:
      type: object
      required:
        - key
        - value
      properties:
        key:
          type: string
        value:
          type: string
    Keyword:
      type: object
      description: Extracted keyword with metadata.
      required:
        - text
        - score
        - algorithm
      properties:
        algorithm:
          $ref: '#/components/schemas/KeywordAlgorithm'
          description: Algorithm that extracted this keyword.
        positions:
          type:
            - array
            - "null"
          items:
            type: integer
            minimum: 0
          description: Optional positions where keyword appears in text (character offsets).
        score:
          type: number
          format: float
          description: Relevance score (higher is better, algorithm-specific range).
        text:
          type: string
          description: The keyword text.
    KeywordAlgorithm:
      type: string
      description: Keyword algorithm selection.
      enum:
        - yake
        - rake
    LanguageConfidence:
      type: object
      description: |-
        Structured per-language detection result: confidence, document share, and script —
        the information the ISO-code-only `detected_languages` list cannot convey (#261).

        Populated by the language-detection processor alongside `detected_languages`, with one
        entry per language, in the same order as `detected_languages`.
      required:
        - language
        - confidence
        - proportion
        - script
        - reliable
      properties:
        confidence:
          type: number
          format: double
          description: |-
            Confidence for this language, in `[0.0, 1.0]`.

            In single-language mode this is whatlang's `Info::confidence()` for the whole
            document. In multi-language mode this is the average whatlang confidence across
            the document's 200-character chunks that were classified as this language.
        language:
          type: string
          description: ISO 639-3 language code, matching the corresponding entry in `detected_languages`.
        proportion:
          type: number
          format: double
          description: |-
            Share of the document's analyzed content classified as this language, in `[0.0, 1.0]`.

            In single-language mode this is always `1.0`. In multi-language mode this is the
            fraction of 200-character chunks classified as this language (chunks that did not
            meet `min_confidence` for any language are excluded from the count but still count
            toward the denominator).
        reliable:
          type: boolean
          description: |-
            Whether this detection is considered reliable.

            In single-language mode this is whatlang's own `Info::is_reliable()` (confidence
            above whatlang's internal 0.9 threshold). In multi-language mode this is the
            chunk-averaged `confidence` above that same 0.9 threshold, since whatlang's
            `is_reliable()` only applies to a single detection.
        script:
          type: string
          description: Writing system whatlang detected for this language (e.g. `"Latin"`, `"Cyrillic"`).
    LayoutRegion:
      type: object
      description: |-
        A detected layout region on a page.

        When layout detection is enabled, each page may have layout regions
        identifying different content types (text, pictures, tables, etc.)
        with confidence scores and spatial positions.
      required:
        - class_name
        - confidence
        - bounding_box
        - area_fraction
      properties:
        area_fraction:
          type: number
          format: double
          description: Fraction of the page area covered by this region (0.0 to 1.0).
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box in document coordinate space.
        class_name:
          type: string
          description: Layout class name (e.g. "picture", "table", "text", "section_header").
        confidence:
          type: number
          format: double
          description: Confidence score from the layout detection model (0.0 to 1.0).
    LinkMetadata:
      type: object
      description: Link element metadata.
      required:
        - href
        - text
        - link_type
        - rel
        - attributes
      properties:
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/KeyValueAttribute'
          description: Additional attributes as key-value pairs.
        href:
          type: string
          description: The href URL value
        link_type:
          $ref: '#/components/schemas/LinkType'
          description: Link type classification
        rel:
          type: array
          items:
            type: string
          description: Rel attribute values
        text:
          type: string
          description: Link text content (normalized)
        title:
          type:
            - string
            - "null"
          description: Optional title attribute
    LinkType:
      type: string
      description: Link type classification.
      enum:
        - anchor
        - internal
        - external
        - email
        - phone
        - other
    ListApiKeysResponse:
      type: object
      description: Response for the paginated API key list endpoint.
      required:
        - api_keys
        - total
        - limit
        - offset
      properties:
        api_keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyResponse'
          description: API keys in the current page.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of API keys for the project.
    ListAuditEntriesResponse:
      type: object
      description: Response for the customer-facing audit read endpoint (`GET /v1/audit`).
      required:
        - entries
        - total
        - limit
        - offset
      properties:
        entries:
          type: array
          items:
            $ref: '#/components/schemas/AuditEntry'
          description: Audit entries in the current page, most recent first.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of audit entries matching the query.
    ListDocumentsQuery:
      type: object
      description: Query parameters for listing integration documents
      properties:
        folder_id:
          type:
            - string
            - "null"
          description: |-
            Source-specific folder ID to restrict results to (e.g., Google Drive folder ID).
            If not provided, lists documents from the root/default location.
          example: "1234567890"
        limit:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum number of documents to return.
        mime_types:
          type:
            - string
            - "null"
          description: |-
            Comma-separated MIME types to filter by (e.g., "text/plain,application/pdf").
            If not provided, all document types are returned.
          example: text/plain,application/pdf
        offset:
          type:
            - integer
            - "null"
          format: int64
          description: Number of matching documents to skip.
    ListDocumentsResponse:
      type: object
      description: |-
        Response body for the integration document list endpoint.

        See the module-level doc for why this intentionally omits
        `total`/`limit`/`offset`.
      required:
        - documents
      properties:
        documents:
          type: array
          items:
            $ref: '#/components/schemas/DocumentResponse'
          description: Documents matching the filter, in source-reported order.
    ListIntegrationsResponse:
      type: object
      description: Response for the paginated integration list endpoint.
      required:
        - integrations
        - total
        - limit
        - offset
      properties:
        integrations:
          type: array
          items:
            $ref: '#/components/schemas/IntegrationResponse'
          description: Integrations in the current page.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of integrations for the project.
    ListInvitationsResponse:
      type: object
      description: Response for the paginated pending-invitation list endpoint.
      required:
        - invitations
        - total
        - limit
        - offset
      properties:
        invitations:
          type: array
          items:
            $ref: '#/components/schemas/InvitationResponse'
          description: Pending invitations in the current page.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of pending invitations for the project.
    ListMembersResponse:
      type: object
      description: Response for the paginated project member list endpoint.
      required:
        - members
        - total
        - limit
        - offset
      properties:
        limit:
          type: integer
          format: int64
          description: Page size used.
        members:
          type: array
          items:
            $ref: '#/components/schemas/MemberResponse'
          description: Members in the current page.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of members in the project.
    ListProjectRolesResponse:
      type: object
      required:
        - roles
        - total
        - limit
        - offset
      properties:
        limit:
          type: integer
          format: int64
        offset:
          type: integer
          format: int64
        roles:
          type: array
          items:
            $ref: '#/components/schemas/ProjectRoleResponse'
        total:
          type: integer
          format: int64
    ListProjectsResponse:
      type: object
      description: Response for the paginated project list endpoint.
      required:
        - projects
        - total
        - limit
        - offset
      properties:
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        projects:
          type: array
          items:
            $ref: '#/components/schemas/ProjectResponse'
          description: Projects in the current page.
        total:
          type: integer
          format: int64
          description: Total number of projects for the caller.
    ListScimAdminGroupsResponse:
      type: object
      required:
        - groups
      properties:
        groups:
          type: array
          items:
            $ref: '#/components/schemas/ScimAdminGroupResponse'
    ListScimConnectionsResponse:
      type: object
      required:
        - connections
      properties:
        connections:
          type: array
          items:
            $ref: '#/components/schemas/ScimConnectionResponse'
    ListWebhookDeliveriesResponse:
      type: object
      description: Response for the paginated webhook delivery-history endpoint.
      required:
        - deliveries
        - total
        - limit
        - offset
      properties:
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliveryResponse'
          description: Delivery attempts in the current page, newest first.
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of delivery attempts recorded for the webhook.
    ListWebhooksResponse:
      type: object
      description: Response for the paginated webhook list endpoint.
      required:
        - webhooks
        - total
        - limit
        - offset
      properties:
        limit:
          type: integer
          format: int64
          description: Page size used.
        offset:
          type: integer
          format: int64
          description: Offset used for pagination.
        total:
          type: integer
          format: int64
          description: Total number of webhooks for the project.
        webhooks:
          type: array
          items:
            $ref: '#/components/schemas/WebhookResponse'
          description: Webhooks in the current page.
    LlmUsage:
      type: object
      description: |-
        Token usage and cost data for a single LLM call made during extraction.

        Populated when VLM OCR, structured extraction, or LLM-based embeddings
        are used. Multiple entries may be present when multiple LLM calls occur
        within one extraction (e.g. VLM OCR + structured extraction).
      required:
        - model
        - source
      properties:
        estimated_cost:
          type:
            - number
            - "null"
          format: double
          description: Estimated cost in USD based on the provider's published pricing.
        finish_reason:
          type:
            - string
            - "null"
          description: Why the model stopped generating (e.g. "stop", "length", "content_filter").
        input_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Number of input/prompt tokens consumed.
          minimum: 0
        model:
          type: string
          description: The LLM model identifier (e.g. "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514").
        output_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Number of output/completion tokens generated.
          minimum: 0
        source:
          type: string
          description: |-
            The pipeline stage that triggered this LLM call
            (e.g. "vlm_ocr", "structured_extraction", "embeddings").
        total_tokens:
          type:
            - integer
            - "null"
          format: int64
          description: Total tokens (input + output).
          minimum: 0
    LocalIdentity:
      type: object
      description: Persisted local operator exposed only when authentication is disabled.
      required:
        - user_id
        - name
        - default_project_id
        - project_name
        - role
      properties:
        default_project_id:
          type: string
          format: uuid
          description: Persisted default workspace identifier.
        email:
          type:
            - string
            - "null"
          description: No external identity provider supplies an email in this mode.
        name:
          type: string
          description: Display name of the local operator.
        project_name:
          type: string
          description: Default workspace display name.
        role:
          type: string
          description: Workspace role of the local operator.
        user_id:
          type: string
          description: Local audit and project ownership principal.
    LoginRequest:
      type: object
      description: Login request with OIDC ID token
      required:
        - id_token
      properties:
        id_token:
          type: string
          description: ID token from OIDC identity provider
          example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
    LoginResponse:
      type: object
      description: Successful login response
      required:
        - token
        - user
        - pending_invitations
      properties:
        pending_invitations:
          type: integer
          format: int32
          description: |-
            Number of pending invitations addressed to this user's email. Non-zero on first
            login when the user was invited; the frontend should route them to the
            accept-invitation page.
          example: 0
        project:
          $ref: '#/components/schemas/ProjectInfo'
          description: |-
            User's default project, or null when the user has pending invitations to accept
            before any project is assigned.
        token:
          type: string
          description: |-
            Backend JWT token (valid for 7 days). The token is always issued; when the user
            has no active project (pending invitations on first login), the token carries a
            nil project sentinel and only endpoints that do not require an active project
            (e.g. accept invitation, create project) will succeed.
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        user:
          $ref: '#/components/schemas/UserInfo'
          description: Authenticated user information
    MarkdownCodeBlock:
      type: object
      required:
        - language
        - code
      properties:
        code:
          type: string
        language:
          type: string
    MarkdownLink:
      type: object
      required:
        - text
        - url
      properties:
        text:
          type: string
        url:
          type: string
    MemberResponse:
      type: object
      description: Project member details.
      required:
        - id
        - user_id
        - role
        - provisioning_source
        - created_at
      properties:
        created_at:
          type: string
          description: ISO 8601 creation timestamp.
        custom_role_id:
          type:
            - string
            - "null"
          format: uuid
          description: First custom role for compatibility with singular-role clients.
        custom_role_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Additive custom roles for a `MEMBER`.
        display_name:
          type:
            - string
            - "null"
          description: Member's display name, when known.
        email:
          type:
            - string
            - "null"
          description: Member's email address, when known.
        id:
          type: string
          description: Unique member identifier.
        provisioning_source:
          type: string
          description: 'Membership authority: `manual` or `scim`.'
        role:
          type: string
          description: Member's role in the project (e.g. `"OWNER"`, `"MEMBER"`).
        user_id:
          type: string
          description: User ID of the member.
    Metadata:
      type: object
      description: |-
        Extraction result metadata.

        Contains common fields applicable to all formats, format-specific metadata
        via a discriminated union, and additional custom fields from postprocessors.
      properties:
        abstract_text:
          type:
            - string
            - "null"
          description: Abstract or summary text (from frontmatter).
        additional:
          type: object
          description: |-
            Additional custom fields from postprocessors.

            Serialized as a nested `"additional"` object (not flattened at root level).
            Uses `Cow<'static, str>` keys so static string keys avoid allocation.
          additionalProperties: {}
          propertyNames:
            type: string
        authors:
          type:
            - array
            - "null"
          items:
            type: string
          description: Primary author(s) - always Vec for consistency
        category:
          type:
            - string
            - "null"
          description: Document category (from frontmatter or classification).
        created_at:
          type:
            - string
            - "null"
          description: Creation timestamp (ISO 8601 format)
        created_by:
          type:
            - string
            - "null"
          description: User who created the document
        document_version:
          type:
            - string
            - "null"
          description: Document version string (from frontmatter).
        error:
          $ref: '#/components/schemas/ErrorMetadata'
          description: Error metadata (for batch operations)
        extraction_duration_ms:
          type:
            - integer
            - "null"
          format: int64
          description: |-
            Extraction duration in milliseconds (for benchmarking).

            This field is populated by batch extraction to provide per-file timing
            information. It's `None` for single-file extraction (which uses external timing).
          minimum: 0
        format:
          $ref: '#/components/schemas/FormatMetadata'
          description: |-
            Format-specific metadata (discriminated union)

            Contains detailed metadata specific to the document format.
            Serialized as a nested `"format"` object with a `format_type` discriminator field.
        image_preprocessing:
          $ref: '#/components/schemas/ImagePreprocessingMetadata'
          description: Image preprocessing metadata (when OCR preprocessing was applied)
        json_schema:
          description: JSON schema (for structured data extraction)
        keywords:
          type:
            - array
            - "null"
          items:
            type: string
          description: Keywords/tags - always Vec for consistency
        language:
          type:
            - string
            - "null"
          description: Primary language (ISO 639 code)
        modified_at:
          type:
            - string
            - "null"
          description: Last modification timestamp (ISO 8601 format)
        modified_by:
          type:
            - string
            - "null"
          description: User who last modified the document
        ocr_used:
          type: boolean
          description: |-
            Whether OCR was used during extraction.

            Set to `true` whenever the extraction pipeline ran an OCR backend
            (Tesseract, PaddleOCR, VLM, etc.) and used that output as the primary
            or fallback text. `false` means native text extraction was used exclusively.
        output_format:
          type:
            - string
            - "null"
          description: |-
            Output format identifier (e.g., "markdown", "html", "text").

            Set by the output format pipeline stage when format conversion is applied.
            Previously stored in `metadata.additional["output_format"]`.
        pages:
          $ref: '#/components/schemas/PageStructure'
          description: Page/slide/sheet structure with boundaries
        subject:
          type:
            - string
            - "null"
          description: Document subject or description
        tags:
          type:
            - array
            - "null"
          items:
            type: string
          description: Document tags (from frontmatter).
        title:
          type:
            - string
            - "null"
          description: Document title
    MimeTypeUsage:
      type: object
      description: Per-MIME-type usage breakdown
      required:
        - documents
        - pages
        - failed
      properties:
        documents:
          type: integer
          format: int32
          description: Number of documents processed
        failed:
          type: integer
          format: int32
          description: Number of failed documents
        pages:
          type: integer
          format: int64
          description: Total pages extracted
    MultiVectorEmbedding:
      type: object
      description: |
        Stub multi-vector result type for builds without the `late-interaction-presets` feature.
      required:
        - num_tokens
        - dim
        - data
      properties:
        data:
          type: array
          items:
            type: number
            format: float
          description: Flat row-major buffer, length `num_tokens * dim`.
        dim:
          type: integer
          format: int32
          description: Dimensionality of each per-token vector.
          minimum: 0
        num_tokens:
          type: integer
          format: int32
          description: Number of attention-live token rows.
          minimum: 0
    NodeContent:
      oneOf:
        - type: object
          description: Document title.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - title
            text:
              type: string
              description: The title text content.
        - type: object
          description: Section heading with level (1-6).
          required:
            - level
            - text
            - node_type
          properties:
            level:
              type: integer
              format: int32
              description: Heading depth (1 = h1, 2 = h2, …, 6 = h6).
              minimum: 0
            node_type:
              type: string
              enum:
                - heading
            text:
              type: string
              description: The heading text content.
        - type: object
          description: Body text paragraph.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - paragraph
            text:
              type: string
              description: The paragraph text content.
        - type: object
          description: List container — children are `ListItem` nodes.
          required:
            - ordered
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - list
            ordered:
              type: boolean
              description: '`true` for ordered (numbered) lists; `false` for unordered (bullet) lists.'
        - type: object
          description: Individual list item.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - list_item
            text:
              type: string
              description: The list item text content.
        - type: object
          description: Table with structured cell grid.
          required:
            - grid
            - node_type
          properties:
            grid:
              $ref: '#/components/schemas/TableGrid'
              description: Structured grid of table cells.
            node_type:
              type: string
              enum:
                - table
        - type: object
          description: Image reference.
          required:
            - node_type
          properties:
            description:
              type:
                - string
                - "null"
              description: Optional alt text or caption describing the image.
            image_index:
              type:
                - integer
                - "null"
              format: int32
              description: Index into the parent `ExtractedDocument::images` list.
              minimum: 0
            node_type:
              type: string
              enum:
                - image
            src:
              type:
                - string
                - "null"
              description: Source URL or path of the image (from `<img src="...">` or `![](src)`).
        - type: object
          description: Code block.
          required:
            - text
            - node_type
          properties:
            language:
              type:
                - string
                - "null"
              description: Programming language identifier (e.g. `"rust"`, `"python"`).
            node_type:
              type: string
              enum:
                - code
            text:
              type: string
              description: The source code text content.
        - type: object
          description: Block quote — container, children carry the quoted content.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - quote
        - type: object
          description: Mathematical formula / equation.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - formula
            text:
              type: string
              description: The formula source text (LaTeX or plain mathematical notation).
        - type: object
          description: Footnote reference content.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - footnote
            text:
              type: string
              description: The footnote body text.
        - type: object
          description: |-
            Reviewer/editor comment content (e.g. DOCX comments).

            Distinct from `NodeContent.Footnote` (xberg-io/xberg#300): comments and
            footnotes both reach the internal document via a marker/definition pair, but
            a consumer needs to tell a reviewer comment apart from an authored footnote.
          required:
            - text
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - comment
            text:
              type: string
              description: The comment body text.
        - type: object
          description: |-
            Logical grouping container (section, key-value area).

            `heading_level` + `heading_text` capture the section heading directly
            rather than relying on a first-child positional convention.
          required:
            - node_type
          properties:
            heading_level:
              type:
                - integer
                - "null"
              format: int32
              description: Heading level of the section heading that opened this group (1-6).
              minimum: 0
            heading_text:
              type:
                - string
                - "null"
              description: Text of the section heading that opened this group.
            label:
              type:
                - string
                - "null"
              description: Optional display label for the group (e.g. section name).
            node_type:
              type: string
              enum:
                - group
        - type: object
          description: Page break marker.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - page_break
        - type: object
          description: Presentation slide container — children are the slide's content nodes.
          required:
            - number
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - slide
            number:
              type: integer
              format: int32
              description: 1-indexed slide number.
              minimum: 0
            title:
              type:
                - string
                - "null"
              description: Slide title text, if present.
        - type: object
          description: Definition list container — children are `DefinitionItem` nodes.
          required:
            - node_type
          properties:
            node_type:
              type: string
              enum:
                - definition_list
        - type: object
          description: Individual definition list entry with term and definition.
          required:
            - term
            - definition
            - node_type
          properties:
            definition:
              type: string
              description: The definition or description of the term.
            node_type:
              type: string
              enum:
                - definition_item
            term:
              type: string
              description: The term being defined.
        - type: object
          description: Citation or bibliographic reference.
          required:
            - key
            - text
            - node_type
          properties:
            key:
              type: string
              description: Citation key (e.g. BibTeX key or reference ID).
            node_type:
              type: string
              enum:
                - citation
            text:
              type: string
              description: Formatted citation text as it appears in the document.
        - type: object
          description: |-
            Admonition / callout container (note, warning, tip, etc.).

            Children carry the admonition body content.
          required:
            - kind
            - node_type
          properties:
            kind:
              type: string
              description: Kind of admonition (e.g. "note", "warning", "tip", "danger").
            node_type:
              type: string
              enum:
                - admonition
            title:
              type:
                - string
                - "null"
              description: Optional explicit title overriding the default kind label.
        - type: object
          description: |-
            Raw block preserved verbatim from the source format.

            Used for content that cannot be mapped to a semantic node type
            (e.g. JSX in MDX, raw LaTeX in markdown, embedded HTML).
          required:
            - format
            - content
            - node_type
          properties:
            content:
              type: string
              description: Verbatim source content in the specified format.
            format:
              type: string
              description: Source format identifier (e.g. "html", "latex", "jsx").
            node_type:
              type: string
              enum:
                - raw_block
        - type: object
          description: Structured metadata block (email headers, YAML frontmatter, etc.).
          required:
            - entries
            - node_type
          properties:
            entries:
              type: array
              items:
                $ref: '#/components/schemas/KeyValueAttribute'
              description: Key-value pairs extracted from the metadata block.
            node_type:
              type: string
              enum:
                - metadata_block
      description: |-
        Tagged enum for node content. Each variant carries only type-specific data.

        Uses `#[serde(tag = "node_type")]` to avoid "type" keyword collision in
        Go/Java/TypeScript bindings.
    NodeIndex:
      type: integer
      format: int32
      description: |-
        Newtype for node indices into the `DocumentStructure::nodes` array.

        Uses `u32` for cross-platform consistency (WASM is 32-bit) and to avoid
        confusion with page numbers or other `usize` values.
      minimum: 0
    OAuthErrorBody:
      type: object
      required:
        - error
        - error_description
      properties:
        error:
          type: string
        error_description:
          type: string
    OcrBoundingGeometry:
      oneOf:
        - type: object
          description: Axis-aligned bounding box (typical for Tesseract output).
          required:
            - left
            - top
            - width
            - height
            - type
          properties:
            height:
              type: integer
              format: int32
              description: Height in pixels
              minimum: 0
            left:
              type: integer
              format: int32
              description: Left x-coordinate in pixels
              minimum: 0
            top:
              type: integer
              format: int32
              description: Top y-coordinate in pixels
              minimum: 0
            type:
              type: string
              enum:
                - rectangle
            width:
              type: integer
              format: int32
              description: Width in pixels
              minimum: 0
        - type: object
          description: |-
            4-point quadrilateral for rotated/skewed text (PaddleOCR).

            Points are in clockwise order starting from top-left:
            `[top_left, top_right, bottom_right, bottom_left]`
          required:
            - points
            - type
          properties:
            points:
              type: array
              items:
                $ref: '#/components/schemas/OcrPoint'
              description: Exactly four corner points in clockwise order.
            type:
              type: string
              enum:
                - quadrilateral
      description: |-
        Bounding geometry for an OCR element.

        Supports both axis-aligned rectangles (from Tesseract) and 4-point quadrilaterals
        (from PaddleOCR and rotated text detection).
    OcrConfidence:
      type: object
      description: |-
        Confidence scores for an OCR element.

        Separates detection confidence (how confident that text exists at this location)
        from recognition confidence (how confident about the actual text content).
      required:
        - recognition
      properties:
        detection:
          type:
            - number
            - "null"
          format: double
          description: |-
            Detection confidence: how confident the OCR engine is that text exists here.

            PaddleOCR provides this as `box_score`, Tesseract doesn't have a direct equivalent.
            Range: 0.0 to 1.0 (or None if not available).
        recognition:
          type: number
          format: double
          description: |-
            Recognition confidence: how confident about the text content.

            Range: 0.0 to 1.0.
    OcrElement:
      type: object
      description: |-
        A unified OCR element representing detected text with full metadata.

        This is the primary type for structured OCR output, preserving all information
        from both Tesseract and PaddleOCR backends.
      required:
        - text
        - geometry
        - confidence
      properties:
        backend_metadata:
          type: object
          description: Backend-specific metadata that doesn't fit the unified schema.
          additionalProperties: {}
          propertyNames:
            type: string
        confidence:
          $ref: '#/components/schemas/OcrConfidence'
          description: Confidence scores for detection and recognition.
        geometry:
          $ref: '#/components/schemas/OcrBoundingGeometry'
          description: Bounding geometry (rectangle or quadrilateral).
        level:
          $ref: '#/components/schemas/OcrElementLevel'
          description: Hierarchical level (word, line, block, page).
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed).
          minimum: 0
        parent_id:
          type:
            - string
            - "null"
          description: |-
            Parent element ID for hierarchical relationships.

            When hierarchy output is enabled, this resolves to another emitted element's
            `backend_metadata["element_id"]` value.
        rotation:
          $ref: '#/components/schemas/OcrRotation'
          description: Rotation information (if detected).
        text:
          type: string
          description: The recognized text content.
    OcrElementLevel:
      type: string
      description: |-
        Hierarchical level of an OCR element.

        Maps to Tesseract's page segmentation hierarchy and provides
        equivalent semantics for PaddleOCR.
      enum:
        - word
        - line
        - block
        - page
    OcrMetadata:
      type: object
      description: |-
        OCR processing metadata.

        Captures information about OCR processing configuration and results.
      required:
        - language
        - psm
        - output_format
        - table_count
      properties:
        language:
          type: string
          description: OCR language code(s) used
        output_format:
          type: string
          description: Output format (e.g., "text", "hocr")
        psm:
          type: integer
          format: int32
          description: Tesseract Page Segmentation Mode (PSM)
        table_cols:
          type:
            - integer
            - "null"
          format: int32
          description: Number of columns in the detected table (if a single table was found).
          minimum: 0
        table_count:
          type: integer
          format: int32
          description: Number of tables detected
          minimum: 0
        table_rows:
          type:
            - integer
            - "null"
          format: int32
          description: Number of rows in the detected table (if a single table was found).
          minimum: 0
    OcrPageFailure:
      type: object
      description: |-
        A page whose OCR failed while extraction of the document continued.

        The warning in `ExtractedDocument::processing_warnings` stays available. One warning
        can cover several pages: when an automatic OCR run of selected pages fails as a whole,
        it adds one warning and one record for each selected page. A failure of the OCR fallback
        for the whole document adds a warning and no record. Repeated failures on one page make
        one record.
      required:
        - page
        - error
        - recovered
      properties:
        error:
          type: string
          description: OCR backend error text.
        page:
          type: integer
          format: int32
          description: One-based page number in the source document.
          minimum: 0
        recovered:
          type: boolean
          description: Whether content for this page was retained or recovered through another route.
    OcrPoint:
      type: object
      required:
        - x
        - y
      properties:
        x:
          type: integer
        y:
          type: integer
    OcrRotation:
      type: object
      description: Rotation information for an OCR element.
      required:
        - angle_degrees
      properties:
        angle_degrees:
          type: number
          format: double
          description: Rotation angle in degrees (0, 90, 180, 270 for PaddleOCR).
        confidence:
          type:
            - number
            - "null"
          format: double
          description: Confidence score for the rotation detection.
    OidcInfo:
      type: object
      description: OIDC login configuration, as visible to an unauthenticated caller.
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: Whether OIDC login is enabled at all.
        issuer:
          type:
            - string
            - "null"
          description: The configured OIDC issuer URL, or `None`.
        provider:
          type:
            - string
            - "null"
          description: |-
            Provider label derived from the configured issuer (`"google"`,
            `"github"`, `"firebase"`, `"oidc"`), or `None` when disabled or running
            in mock mode without a configured issuer.
    PageBoundary:
      type: object
      description: |-
        Byte offset boundary for a page.

        Tracks where a specific page's content starts and ends in the main content string,
        enabling mapping from byte positions to page numbers. Offsets are guaranteed to be
        at valid UTF-8 character boundaries when using standard String methods (push_str, push, etc.).
      required:
        - byte_start
        - byte_end
        - page_number
      properties:
        byte_end:
          type: integer
          description: Byte offset where this page ends in the content string (UTF-8 valid boundary, exclusive)
          minimum: 0
        byte_start:
          type: integer
          description: Byte offset where this page starts in the content string (UTF-8 valid boundary, inclusive)
          minimum: 0
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
    PageClassification:
      type: object
      description: Classification result for a single page.
      required:
        - page_number
        - labels
      properties:
        labels:
          type: array
          items:
            $ref: '#/components/schemas/ClassificationLabel'
          description: |-
            Labels assigned to the page. Single-label classification yields exactly one
            entry; multi-label classification yields any subset of the configured label set.
        page_number:
          type: integer
          format: int32
          description: 1-indexed page number this classification belongs to.
          minimum: 0
    PageContent:
      type: object
      description: |-
        Content for a single page/slide.

        When page extraction is enabled, documents are split into per-page content
        with associated tables and images mapped to each page.

        # Performance

        Uses Arc-wrapped tables and images for memory efficiency:
        - `Vec<Arc<Table>>` enables zero-copy sharing of table data
        - `Vec<Arc<ExtractedImage>>` enables zero-copy sharing of image data
        - Maintains exact JSON compatibility via custom Serialize/Deserialize

        This reduces memory overhead for documents with shared tables/images
        by avoiding redundant copies during serialization.
      required:
        - page_number
        - content
      properties:
        content:
          type: string
          description: Text content for this page
        hierarchy:
          $ref: '#/components/schemas/PageHierarchy'
          description: |-
            Hierarchy information for the page (when hierarchy extraction is enabled)

            Contains text hierarchy levels (H1-H6) extracted from the page content.
        image_indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Indices into `ExtractedDocument.images` for images found on this page.

            Each value is a zero-based index into the top-level `images` collection.
            Only populated when `extract_images = true` in the extraction config.
        image_preprocessing:
          $ref: '#/components/schemas/ImagePreprocessingMetadata'
          description: OCR image preprocessing applied to this page's raster.
        is_blank:
          type:
            - boolean
            - "null"
          description: |-
            Whether this page is blank (no meaningful text content)

            Determined during extraction based on text content analysis.
            A page is blank if it has fewer than 3 non-whitespace characters
            and contains no tables or images.
        layout_regions:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/LayoutRegion'
          description: |-
            Layout detection regions for this page (when layout detection is enabled).

            Contains detected layout regions with class, confidence, bounding box,
            and area fraction. Only populated when layout detection is configured.
        native_content:
          type:
            - string
            - "null"
          description: |-
            Text layer of this page, kept when OCR output replaced it in `content`.

            Set only when `PageConfig::keep_native_content` is on, an OCR route replaced this page's `content`, and the
            text layer is not blank and differs from the OCR text. `None` on every other page. PDF only.
        ocr_confidence:
          $ref: '#/components/schemas/PageOcrConfidence'
          description: Aggregate OCR confidence for this page. `None` when the page was not OCR'd.
        page_number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
        section_name:
          type:
            - string
            - "null"
          description: |-
            Section name this slide belongs to (PPTX only).

            PowerPoint sections group slides into logical chapters (`<p:sectionLst>` in
            `ppt/presentation.xml`). Only populated when the source is a PPTX file and
            the slide belongs to a named section.
        sheet_name:
          type:
            - string
            - "null"
          description: |-
            Sheet name for this page (XLSX/ODS only).

            Each spreadsheet sheet maps to one `PageContent` entry. This field carries the
            sheet's display name as it appears in the workbook. `None` for all non-spreadsheet
            formats and for sheets with an empty name.
        speaker_notes:
          type:
            - string
            - "null"
          description: |-
            Speaker notes for this slide (PPTX only).

            Contains the text from the slide's notes pane (`ppt/notesSlides/notesSlide{N}.xml`).
            Only populated when the source is a PPTX file and notes are present.
        tables:
          type: array
          items:
            $ref: '#/components/schemas/Table'
          description: |-
            Tables found on this page (uses Arc for memory efficiency)

            Serializes as `Vec<Table>` for JSON compatibility while maintaining
            Arc semantics in-memory for zero-copy sharing.
    PageDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: number
        width:
          type: number
    PageHierarchy:
      type: object
      description: |-
        Page hierarchy structure containing heading levels and block information.

        Used when PDF text hierarchy extraction is enabled. Contains hierarchical
        blocks with heading levels (H1-H6) for semantic document structure.
      required:
        - block_count
      properties:
        block_count:
          type: integer
          format: int32
          description: Number of hierarchy blocks on this page
          minimum: 0
        blocks:
          type: array
          items:
            $ref: '#/components/schemas/HierarchicalBlock'
          description: Hierarchical blocks with heading levels
    PageInfo:
      type: object
      required:
        - number
      properties:
        dimensions:
          $ref: '#/components/schemas/PageDimensions'
          description: Dimensions in points (PDF) or pixels (images).
        has_vector_graphics:
          type: boolean
          description: |-
            Whether this page contains non-trivial vector graphics (paths, shapes, curves)

            Indicates the presence of vector-drawn content such as charts, diagrams,
            or geometric shapes (e.g., from Adobe InDesign, LaTeX TikZ). These are
            invisible to `ExtractedDocument.images` since they are not embedded as raster
            XObjects. Set to `true` when path count exceeds a heuristic threshold,
            signaling that downstream consumers may want to rasterize the page to
            capture this content.

            Only populated for PDFs; `None` for other document types.
        hidden:
          type:
            - boolean
            - "null"
          description: Whether this page is hidden (e.g., in presentations)
        image_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of images on this page
          minimum: 0
        is_blank:
          type:
            - boolean
            - "null"
          description: |-
            Whether this page is blank (no meaningful text, no images, no tables)

            A page is considered blank if it has fewer than 3 non-whitespace characters
            and contains no tables or images. This is useful for filtering out empty pages
            in scanned documents or PDFs with blank separator pages.
        number:
          type: integer
          format: int32
          description: Page number (1-indexed)
          minimum: 0
        table_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of tables on this page
          minimum: 0
        title:
          type:
            - string
            - "null"
          description: Page title (usually for presentations)
    PageOcrConfidence:
      type: object
      description: |-
        Aggregate OCR legibility score for a page, reported by the backend that produced its text.

        This is distinct from `OcrConfidence`, which scores a single detected element (a word or
        line) using detection/recognition confidence from the OCR engine itself. `PageOcrConfidence`
        is a page-level summary computed after noise filtering, intended for triage of which pages
        are worth a closer look, not for comparing OCR engines against each other.
      required:
        - word_count
        - backend
      properties:
        backend:
          type: string
          description: Name of the OCR backend that produced the page text.
        score:
          type:
            - number
            - "null"
          format: double
          description: |-
            Aggregate legibility score in `0.0..=1.0`, or `None` when the backend that
            produced this page does not report a calibrated legibility scale.

            Backends differ in what their confidence numbers mean (see `OcrConfidence`'s
            per-backend normalization), and not every backend maps onto a 0.0-1.0 legibility
            scale at all. When a backend has no such calibrated scale, this is `None` rather
            than a misleading number, and scores must never be compared across backends.
        word_count:
          type: integer
          format: int32
          description: |-
            Number of words the score was averaged over, AFTER noise filtering.

            A small `word_count` means the average is based on little evidence, so a high
            `score` next to a small `word_count` is not representative of the whole page.
          minimum: 0
    PageSpan:
      type: object
      description: |-
        A single page covered by a chunk, with an optional bounding box on that page.

        See `ChunkMetadata.page_spans` (#1295) for population semantics.
      required:
        - page
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box on this page, if known.
        page:
          type: integer
          format: int32
          description: Page number (1-indexed).
          minimum: 0
    PageStructure:
      type: object
      description: |-
        Unified page structure for documents.

        Supports different page types (PDF pages, PPTX slides, Excel sheets)
        with character offset boundaries for chunk-to-page mapping.
      required:
        - total_count
        - unit_type
      properties:
        boundaries:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageBoundary'
          description: |-
            Character offset boundaries for each page

            Maps character ranges in the extracted content to page numbers.
            Used for chunk page range calculation.
        pages:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PageInfo'
          description: Detailed per-page metadata (optional, only when needed)
        total_count:
          type: integer
          format: int32
          description: Total number of pages/slides/sheets
          minimum: 0
        unit_type:
          $ref: '#/components/schemas/PageUnitType'
          description: Type of paginated unit
    PageUnitType:
      type: string
      description: |-
        Type of paginated unit in a document.

        Distinguishes between different types of "pages" (PDF pages, presentation slides, spreadsheet sheets).
      enum:
        - page
        - slide
        - sheet
    PdfAnnotation:
      type: object
      description: A PDF annotation extracted from a document page.
      required:
        - annotation_type
        - page_number
      properties:
        annotation_type:
          $ref: '#/components/schemas/PdfAnnotationType'
          description: The type of annotation.
        author:
          type:
            - string
            - "null"
          description: Author/creator of the annotation (PDF `/T` entry).
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: Bounding box of the annotation on the page.
        color:
          type:
            - string
            - "null"
          description: |-
            Annotation colour (PDF `/C` entry), normalised to a CSS-compatible
            `#rrggbb` hex string. Gray and CMYK colour spaces are converted to RGB.
        content:
          type:
            - string
            - "null"
          description: Text content of the annotation (e.g., comment text, link URL).
        marked_text:
          type:
            - string
            - "null"
          description: |-
            The document text covered by `quad_points`, recovered from the
            page content underneath the marked-up region. Populated for
            Highlight, Underline, StrikeOut, and Squiggly annotations when the
            underlying text could be recovered.
        modified:
          type:
            - string
            - "null"
          description: |-
            Last modification date of the annotation (PDF `/M` entry), as a raw
            PDF date string (e.g. `"D:20240115120000Z"`).
        page_number:
          type: integer
          format: int32
          description: Page number where the annotation appears (1-indexed).
          minimum: 0
        quad_points:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/BoundingBox'
          description: |-
            Per-line bounding boxes derived from the annotation's `/QuadPoints`
            entry. Present for text markup annotations (Highlight, Underline,
            StrikeOut, Squiggly), one box per marked line/run of text.
        subject:
          type:
            - string
            - "null"
          description: Subject of the annotation (PDF `/Subj` entry).
    PdfAnnotationType:
      type: string
      description: Type of PDF annotation.
      enum:
        - text
        - highlight
        - link
        - stamp
        - underline
        - strike_out
        - squiggly
        - ink
        - square
        - circle
        - polygon
        - poly_line
        - line
        - caret
        - file_attachment
        - sound
        - movie
        - other
    PdfFormField:
      type: object
      description: |
        A form field extracted from a PDF's AcroForm or XFA structure.

        Populated by the PDF extractor when `PdfConfig::extract_form_fields` is
        enabled and the document is a fillable form. Supports both AcroForm (standard)
        and XFA (XML Forms Architecture) layers. When both are present, AcroForm fields
        take priority (canonical fallback per PDF spec), and XFA-only fields are appended.
        The collection is empty for non-form PDFs and for non-PDF formats.
      required:
        - name
        - full_name
        - field_type
      properties:
        bbox:
          $ref: '#/components/schemas/BoundingBox'
          description: Widget bounding box on its page, if known.
        default_value:
          type:
            - string
            - "null"
          description: Default field value, if any.
        field_type:
          $ref: '#/components/schemas/FormFieldType'
          description: Classified field type.
        flags:
          type: integer
          format: int32
          description: Raw field-flags bitmask (read-only, required, multiline, …).
          minimum: 0
        full_name:
          type: string
          description: Fully-qualified field name (dotted path from the form root).
        max_length:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum input length for text fields, if specified.
          minimum: 0
        name:
          type: string
          description: Partial field name (the leaf name within the field hierarchy).
        page:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            1-indexed page the field's widget appears on. Currently always `None` for
            AcroForm fields; page assignment is a deferred enhancement requiring spatial
            analysis of widget annotations per page.
          minimum: 0
        tooltip:
          type:
            - string
            - "null"
          description: Tooltip / alternate field description, if present.
        value:
          type:
            - string
            - "null"
          description: Current field value, if any.
    PdfMetadata:
      type: object
      description: |-
        PDF-specific metadata.

        Contains metadata fields specific to PDF documents that are not in the common
        `Metadata` structure. Common fields like title, authors, keywords, and dates
        are at the `Metadata` level.
      properties:
        fabricated_text_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages whose text was dominated by fabricated character mappings (1-indexed):
            `MappingProvenance::Fallback`, a font whose glyph-to-Unicode mapping resolved to
            a value the extractor chose rather than read from the file (issue #1254). This is
            a fact about how the text was derived, independent of `scanned_pages`'s raster-based
            scan detection, and independent of whether the resulting text happens to look
            structurally like prose (issue #1667: a broken mapping that lands on ordinary
            letters and punctuation passes every character-shape check but is still fabricated).

            `None` when `OcrQualityThresholds::enable_provenance_ocr_routing` is `false`; empty
            when no page qualifies.
        height:
          type:
            - integer
            - "null"
          format: int64
          description: First page height in points (1/72 inch)
        implausible_text_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages whose native text layer reads as no real detectable language (1-indexed): a
            `/ToUnicode` CMap (or other mapping tier) that resolves every glyph to *a* character,
            consistently the WRONG one (e.g. a ROT-shifted mapping), so the page is structurally
            indistinguishable from real prose to `fabricated_text_pages`'s provenance check and to
            every character-shape heuristic (issue #1696; issue #1667's `quality_score: 1.0` with
            no warning on such a page is the same underlying gap). This is a content-plausibility
            fact, independent of `fabricated_text_pages` and of `scanned_pages`.

            `None` when `OcrQualityThresholds::enable_plausibility_ocr_routing` is `false`; empty
            when no page qualifies.
        is_encrypted:
          type:
            - boolean
            - "null"
          description: Whether the PDF is encrypted/password-protected
        layout_gate_reasons:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            Why the `auto` layout gate selected or skipped each page.

            Index `i` is page `i + 1`. Snake_case values such as `multi_column`,
            `table_grid`, or `plain_text` (the skip reason). `None` unless layout
            detection ran with `LayoutStrategy::Auto`.
        layout_gated_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages the `auto` layout strategy skipped (1-indexed).

            `None` unless layout detection ran with `LayoutStrategy::Auto`; empty
            when the gate selected every page.
        page_count:
          type:
            - integer
            - "null"
          format: int32
          description: Total number of pages in the PDF document
          minimum: 0
        pdf_version:
          type:
            - string
            - "null"
          description: PDF version (e.g., "1.7", "2.0")
        producer:
          type:
            - string
            - "null"
          description: PDF producer (application that created the PDF)
        scanned_confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            How strongly the document's most scan-like page resembles a scan, in `[0.0, 1.0]`.

            `None` when the document could not be inspected. A full-page raster with no
            visible text scores at least `0.85`; a born-digital slide with a full-bleed
            background image scores `0.50`.
        scanned_pages:
          type:
            - array
            - "null"
          items:
            type: integer
            format: int32
            minimum: 0
          description: |-
            Pages that look like scans (1-indexed), using the default confidence threshold.

            `None` when the document could not be inspected; empty when no page qualifies.
        width:
          type:
            - integer
            - "null"
          format: int64
          description: First page width in points (1/72 inch)
    PiiCategory:
      oneOf:
        - type: string
          description: Email address (e.g. `user@example.com`).
          enum:
            - email
        - type: string
          description: Phone number in any common format.
          enum:
            - phone
        - type: string
          description: US Social Security Number.
          enum:
            - ssn
        - type: string
          description: Payment card number (Visa, Mastercard, Amex, etc.).
          enum:
            - credit_card
        - type: string
          description: Postal / ZIP code.
          enum:
            - postal_code
        - type: string
          description: IPv4 or IPv6 address.
          enum:
            - ip_address
        - type: string
          description: International Bank Account Number.
          enum:
            - iban
        - type: string
          description: SWIFT / BIC bank identifier code.
          enum:
            - swift_bic
        - type: string
          description: Date of birth.
          enum:
            - date_of_birth
        - type: string
          description: Person name, surfaced by the optional NER backend.
          enum:
            - person
        - type: string
          description: Organization name, surfaced by the optional NER backend.
          enum:
            - organization
        - type: string
          description: Location, surfaced by the optional NER backend.
          enum:
            - location
        - type: object
          description: |-
            Caller-supplied custom category (e.g. internal employee IDs).

            Surfaced by the redaction engine when a hit comes from
            custom term rules
            or custom pattern rules.
            The string is the label passed alongside the term/pattern. Use those
            fields rather than constructing `Custom` directly via the
            `categories` filter — the pattern engine cannot detect arbitrary text
            from a category name alone.
          required:
            - custom
          properties:
            custom:
              type: string
              description: |-
                Caller-supplied custom category (e.g. internal employee IDs).

                Surfaced by the redaction engine when a hit comes from
                custom term rules
                or custom pattern rules.
                The string is the label passed alongside the term/pattern. Use those
                fields rather than constructing `Custom` directly via the
                `categories` filter — the pattern engine cannot detect arbitrary text
                from a category name alone.
      description: PII categories the pattern engine recognises.
    PixelDimensions:
      type: object
      required:
        - width
        - height
      properties:
        height:
          type: integer
        width:
          type: integer
    PlanInfo:
      type: object
      description: Plan limits for the project
      properties:
        max_file_size_mb:
          type:
            - integer
            - "null"
          format: int32
          description: Maximum file size in megabytes (null = unlimited)
        max_monthly_jobs:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum jobs allowed per month (null = unlimited)
        max_monthly_pages:
          type:
            - integer
            - "null"
          format: int64
          description: Maximum pages allowed per month (null = unlimited)
    PortalResponse:
      type: object
      description: Response body for a Stripe Customer Portal Session
      required:
        - url
      properties:
        url:
          type: string
          description: Stripe Customer Portal URL to redirect the user to
    PptxMetadata:
      type: object
      description: |-
        PowerPoint presentation metadata.

        Extracted from PPTX files containing slide counts and presentation details.
      required:
        - slide_count
        - slide_names
      properties:
        image_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of embedded images
          minimum: 0
        slide_count:
          type: integer
          format: int32
          description: Total number of slides in the presentation
          minimum: 0
        slide_names:
          type: array
          items:
            type: string
          description: Names of slides (if available)
        table_count:
          type:
            - integer
            - "null"
          format: int32
          description: Number of tables
          minimum: 0
    ProcessingStatsEntry:
      type: object
      description: Processing time percentiles for a day
      required:
        - sample_count
      properties:
        p50_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Median processing time in milliseconds
        p95_ms:
          type:
            - integer
            - "null"
          format: int64
          description: 95th percentile processing time in milliseconds
        p99_ms:
          type:
            - integer
            - "null"
          format: int64
          description: 99th percentile processing time in milliseconds
        sample_count:
          type: integer
          format: int32
          description: Number of samples used for calculation
    ProcessingWarning:
      type: object
      description: |-
        A non-fatal warning from a processing pipeline stage.

        Captures errors from optional features that don't prevent extraction
        but may indicate degraded or incomplete results. Inspect these independently
        from `ExtractedDocument::quality_score`, which assesses retained text only.
      required:
        - source
        - message
      properties:
        message:
          type: string
          description: Human-readable description of what went wrong.
        source:
          type: string
          description: |-
            The pipeline stage or feature that produced this warning
            (e.g., "embedding", "chunking", "language_detection", "output_format").
    ProjectInfo:
      type: object
      description: Project information in login response
      required:
        - id
        - name
        - role
      properties:
        id:
          type: string
          format: uuid
          description: Project UUID
          example: 550e8400-e29b-41d4-a716-446655440000
        name:
          type: string
          description: Project name
          example: My Project
        role:
          type: string
          description: User role in this project
          example: OWNER
    ProjectPermission:
      type: string
      enum:
        - project.read
        - project.update
        - project.delete
        - rag.read
        - rag.update
        - extraction.execute
        - extraction.read
        - job.read
        - job.cancel
        - document.read
        - preset.read
        - preset.manage
        - auto_tune.read
        - auto_tune.manage
        - member.read
        - member.update
        - member.remove
        - member.leave
        - role.read
        - role.create
        - role.update
        - role.delete
        - role.assign
        - invitation.read
        - invitation.create
        - invitation.revoke
        - api_key.read
        - api_key.create
        - api_key.regenerate
        - api_key.revoke
        - audit.read
        - scim.read
        - scim.manage
        - integration.read
        - integration.create
        - integration.delete
        - integration.connect
        - integration.disconnect
        - integration_document.read
        - webhook.read
        - webhook.create
        - webhook.update
        - webhook.delete
        - webhook.test
        - webhook_delivery.read
        - webhook_delivery.retry
        - usage.read
        - analytics.read
        - sandbox.execute
        - billing.read
        - billing.checkout
        - billing.portal
    ProjectResponse:
      type: object
      description: |-
        Project details, as returned by create/get/list.

        Mirrors the Enterprise backend's `ProjectResponse` exactly. Pro's current
        HTTP DTOs (`ProjectSummary`, `CreateProjectResponse`) expose a subset of
        these fields, but Pro's `persistence::ProjectSummary` model already tracks
        every one of them (`owner_user_id`, `api_key_count`, `webhook_count`,
        `total_pages_extracted`) — adopting this full shape is a handler-level
        wiring change, not a schema gap, so no field here is `Option`.
      required:
        - id
        - name
        - slug
        - status
        - owner_user_id
        - created_at
        - updated_at
        - api_key_count
        - webhook_count
        - total_pages_extracted
      properties:
        api_key_count:
          type: integer
          format: int32
          description: Number of active (non-revoked) API keys.
        created_at:
          type: string
          format: date-time
          description: ISO 8601 creation timestamp.
        id:
          type: string
          format: uuid
          description: Unique project identifier.
        name:
          type: string
          description: Human-readable project name.
        owner_user_id:
          type: string
          description: User ID of the project owner (from the identity provider).
        slug:
          type: string
          description: URL-safe identifier.
        status:
          type: string
          description: Project status (e.g. `"ACTIVE"`, `"SUSPENDED"`, `"INACTIVE"`).
        total_pages_extracted:
          type: integer
          format: int64
          description: Total billable pages extracted (excluding cancelled jobs).
        updated_at:
          type: string
          format: date-time
          description: ISO 8601 last update timestamp.
        webhook_count:
          type: integer
          format: int32
          description: Number of active (non-deleted) webhooks.
    ProjectRoleResponse:
      type: object
      required:
        - id
        - name
        - permissions
      properties:
        description:
          type:
            - string
            - "null"
        id:
          type: string
          format: uuid
        name:
          type: string
        permissions:
          type: array
          items:
            $ref: '#/components/schemas/ProjectPermission'
    PropertyChange:
      type: object
      description: |-
        A single run-level or style-level property change.

        Used for revisions that change formatting rather than text content. `from`
        and `to` store normalized property values when the source format exposes
        them; either side may be absent when the format only records one side of the
        change.
      required:
        - name
      properties:
        from:
          type:
            - string
            - "null"
          description: Value before the change, when available.
        name:
          type: string
          description: Property name, such as `"bold"`, `"italic"`, `"font_size"`, or `"font_color"`.
        to:
          type:
            - string
            - "null"
          description: Value after the change, when available.
    PstMetadata:
      type: object
      description: Outlook PST archive metadata.
      required:
        - message_count
      properties:
        message_count:
          type: integer
          description: Total number of email messages found in the PST archive.
          minimum: 0
    PublicSandboxExtractForm:
      type: object
      description: |-
        Multipart form fields accepted by the public sandbox extract endpoint.

        Documented so the generated OpenAPI client is aware of every field; the
        handler parses the multipart body manually rather than deserializing this
        type. `file` is required for every mode except `web`, which reads `url`.
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: Document to extract (required for all modes except `web`). Max 1 MB.
        mode:
          type:
            - string
            - "null"
          description: 'Extraction mode: `extract` (default), `rag`, `idp`, `code`, `web`.'
          example: rag
        preset:
          type:
            - string
            - "null"
          description: Curated preset id — required for `idp` mode (validated against the embedded registry).
          example: invoice
        url:
          type:
            - string
            - "null"
          description: Seed URL to crawl — required for `web` mode.
          example: https://example.com
    QrBoundingBox:
      type: object
      description: Pixel-space bounding box of a QR code inside its source image.
      required:
        - x
        - y
        - width
        - height
      properties:
        height:
          type: integer
          format: int32
          description: Height of the bounding box in pixels.
          minimum: 0
        width:
          type: integer
          format: int32
          description: Width of the bounding box in pixels.
          minimum: 0
        x:
          type: integer
          format: int32
          description: Horizontal pixel offset of the bounding box top-left corner.
          minimum: 0
        y:
          type: integer
          format: int32
          description: Vertical pixel offset of the bounding box top-left corner.
          minimum: 0
    QrCode:
      type: object
      description: One QR code decoded from an extracted image.
      required:
        - payload
      properties:
        bbox:
          $ref: '#/components/schemas/QrBoundingBox'
          description: |-
            Bounding box of the QR code inside the source image, in pixel coordinates
            (`x`, `y` of the top-left corner; `width`, `height` of the rectangle).
            `None` if the decoder did not report a bounding box.
        confidence:
          type:
            - number
            - "null"
          format: float
          description: |-
            Detector-reported confidence in `[0.0, 1.0]`. `None` when the decoder
            does not expose confidence (the default `rqrr` backend always reports
            `Some` because successful decode implies high confidence).
        payload:
          type: string
          description: Decoded payload (text, URL, vCard string, …).
    RagConfigResponse:
      type: object
      description: |-
        A project's RAG configuration, as returned by `GET .../rag-config` and
        `PUT .../rag-config`.

        A plain, sqlx-free mirror of `common::ProjectRagConfig` — `api-types` must
        not depend on `sqlx`/`tokio-postgres`, so the shape is duplicated here
        rather than shared.
      required:
        - enabled
      properties:
        default_collection:
          type:
            - string
            - "null"
          description: Default vector collection for RAG operations.
        enabled:
          type: boolean
          description: Whether RAG is enabled for this project.
    ReadinessChecks:
      type: object
      description: Individual dependency check results
      required:
        - database
        - maintenance_database
      properties:
        database:
          type: string
          description: Database connectivity
          example: ok
        maintenance_database:
          type: string
          description: Maintenance database connectivity.
          example: ok
    ReadinessResponse:
      type: object
      description: Readiness check response with dependency status
      required:
        - status
        - checks
      properties:
        checks:
          $ref: '#/components/schemas/ReadinessChecks'
          description: Dependency check results
        status:
          type: string
          description: Overall readiness status
          example: ready
    RedactionFinding:
      type: object
      description: 'One redaction event: which span was rewritten, why, and with what.'
      required:
        - start
        - end
        - category
        - strategy
        - replacement_token
      properties:
        category:
          $ref: '#/components/schemas/PiiCategory'
          description: PII category that fired this redaction.
        end:
          type: integer
          format: int32
          description: Byte-offset end (exclusive) in the original `ExtractedDocument::content`.
          minimum: 0
        replacement_token:
          type: string
          description: |-
            String that replaced the original mention. Always present; for `Drop` the
            replacement is the empty string.
        start:
          type: integer
          format: int32
          description: Byte-offset start in the original (pre-redaction) `ExtractedDocument::content`.
          minimum: 0
        strategy:
          $ref: '#/components/schemas/RedactionStrategy'
          description: Strategy applied to this finding (mask, hash, token-replace, drop).
    RedactionReport:
      type: object
      description: |-
        Audit report describing what the redaction processor found and how it replaced it.

        The redactor returns this alongside the rewritten content so compliance, replay, and
        audit-log consumers can see exactly what fired. Offsets are relative to the *original*
        pre-redaction `content` and are intended for audit reconstruction only — the original
        bytes are dropped at the end of the pipeline.
      required:
        - findings
        - total_redacted
      properties:
        findings:
          type: array
          items:
            $ref: '#/components/schemas/RedactionFinding'
          description: Individual redaction findings in original-source byte order.
        total_redacted:
          type: integer
          format: int32
          description: Total number of redactions applied across the document.
          minimum: 0
    RedactionStrategy:
      type: string
      description: Strategy applied when a PII match is rewritten.
      enum:
        - mask
        - hash
        - token_replace
        - drop
    RelationshipKind:
      type: string
      description: Semantic kind of a relationship between document elements.
      enum:
        - footnote_reference
        - citation_reference
        - internal_link
        - caption
        - label
        - toc_entry
        - cross_reference
    ReplaceScimGroupRolesRequest:
      type: object
      required:
        - role_ids
      properties:
        role_ids:
          type: array
          items:
            type: string
            format: uuid
    RetryWebhookDeliveryResponse:
      type: object
      description: |-
        A manual redelivery request has been queued. Delivery itself happens out
        of band (the same NATS-consumer delivery path automatic retries use), so
        this is an accepted-style response rather than a completed delivery
        record: the caller polls `GET .../deliveries` to observe the outcome.
      required:
        - delivery_id
        - status
        - queued_at
      properties:
        delivery_id:
          type: string
          format: uuid
          description: Identifier of the new delivery attempt created for this retry.
        queued_at:
          type: string
          description: ISO 8601 timestamp the retry was queued at.
        status:
          type: string
          description: |-
            Always `"queued"` today; a stable string so future states
            (`"delivered"`, `"failed"`) can be added without a breaking change.
          example: queued
    RevisionAnchor:
      oneOf:
        - type: object
          description: Body paragraph, identified by its zero-based index in the document flow.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based index of the paragraph in document order.
              minimum: 0
            type:
              type: string
              enum:
                - paragraph
        - type: object
          description: Cell inside a table.
          required:
            - row
            - col
            - table_index
            - type
          properties:
            col:
              type: integer
              description: Zero-based column index within the table.
              minimum: 0
            row:
              type: integer
              description: Zero-based row index within the table.
              minimum: 0
            table_index:
              type: integer
              description: Zero-based index of the table in document order.
              minimum: 0
            type:
              type: string
              enum:
                - table_cell
        - type: object
          description: Page, identified by its zero-based index.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based page index.
              minimum: 0
            type:
              type: string
              enum:
                - page
        - type: object
          description: Presentation slide, identified by its zero-based index.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based slide index.
              minimum: 0
            type:
              type: string
              enum:
                - slide
        - type: object
          description: Spreadsheet cell or range, identified by sheet index and optional name.
          required:
            - index
            - type
          properties:
            index:
              type: integer
              description: Zero-based sheet index.
              minimum: 0
            name:
              type:
                - string
                - "null"
              description: Sheet display name when available.
            type:
              type: string
              enum:
                - sheet
      description: Best-effort document location for a revision.
    RevisionDelta:
      type: object
      description: |-
        The content changes that make up a single revision.

        For insertions and deletions the `content` field carries the added/removed
        lines as `DiffLine::Added` / `DiffLine::Removed` entries. For format
        changes, `property_changes` carries normalized before/after formatting
        values when the source document exposes them.
      required:
        - content
        - table_changes
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/DiffLine'
          description: Line-level content changes for this revision.
        property_changes:
          type: array
          items:
            $ref: '#/components/schemas/PropertyChange'
          description: Formatting or metadata property changes for this revision.
        table_changes:
          type: array
          items:
            $ref: '#/components/schemas/CellChange'
          description: Cell-level table changes for this revision.
    RevisionKind:
      type: string
      description: Semantic classification of a tracked change.
      enum:
        - insertion
        - deletion
        - format_change
        - comment
    SamlAcsForm:
      type: object
      required:
        - SAMLResponse
      properties:
        RelayState:
          type:
            - string
            - "null"
        SAMLResponse:
          type: string
    SamlDomainRequest:
      type: object
      required:
        - provider_id
        - domain
      properties:
        domain:
          type: string
        provider_id:
          type: string
          format: uuid
      additionalProperties: false
    SamlDomainResponse:
      type: object
      required:
        - domain
        - provider_id
      properties:
        dns_txt_name:
          type:
            - string
            - "null"
        dns_txt_value:
          type:
            - string
            - "null"
        domain:
          type: string
        provider_id:
          type: string
          format: uuid
        verified_at:
          type:
            - string
            - "null"
          format: date-time
    SamlExchangeRequest:
      type: object
      required:
        - code
      properties:
        code:
          type: string
      additionalProperties: false
    SamlExchangeResponse:
      type: object
      required:
        - token
        - project_id
        - user_id
        - role
      properties:
        email:
          type:
            - string
            - "null"
        name:
          type:
            - string
            - "null"
        project_id:
          type: string
          format: uuid
        redirect_path:
          type:
            - string
            - "null"
        role:
          type: string
        token:
          type: string
        user_id:
          type: string
    SamlInfo:
      type: object
      description: SAML login configuration visible before authentication.
      required:
        - enabled
        - label
        - login_url
      properties:
        enabled:
          type: boolean
        label:
          type: string
        login_url:
          type: string
    SamlProviderRequest:
      type: object
      required:
        - label
        - metadata_xml
      properties:
        enabled:
          type: boolean
        enforce_sso:
          type: boolean
        idp_initiated_enabled:
          type: boolean
        jit_enabled:
          type: boolean
        label:
          type: string
        metadata_xml:
          type: string
      additionalProperties: false
    SamlProviderResponse:
      type: object
      required:
        - id
        - project_id
        - label
        - entity_id
        - enabled
        - jit_enabled
        - idp_initiated_enabled
        - enforce_sso
        - acs_url
        - idp_initiated_relay_state
      properties:
        acs_url:
          type: string
        enabled:
          type: boolean
        enforce_sso:
          type: boolean
        entity_id:
          type: string
        id:
          type: string
          format: uuid
        idp_initiated_enabled:
          type: boolean
        idp_initiated_relay_state:
          type: string
        jit_enabled:
          type: boolean
        label:
          type: string
        project_id:
          type: string
          format: uuid
    SandboxExtractResponse:
      type: object
      description: Sandbox extraction response — mirrors the real API `GET /v1/jobs/{id}` response
      required:
        - id
        - filename
        - status
        - created_at
      properties:
        created_at:
          type: string
          description: Job creation timestamp (RFC3339)
          example: 2025-12-21T10:00:00Z
        filename:
          type: string
          description: Original filename
          example: invoice.pdf
        id:
          type: string
          description: Unique job identifier (UUID)
          example: 550e8400-e29b-41d4-a716-446655440000
        processing_time_ms:
          type:
            - integer
            - "null"
          format: int64
          description: Processing time in milliseconds
          example: 1234
        result:
          oneOf:
            - $ref: '#/components/schemas/ExtractedDocument'
              description: Extraction result (only present when completed)
            - type: "null"
        status:
          type: string
          description: Job status
          example: completed
    SchemaCompliance:
      type: string
      description: |-
        Schema-validation outcome surfaced as one of three buckets.

        Fold into the combined confidence score without leaking internal validation
        error types.
      enum:
        - all_valid
        - partial_valid
        - all_invalid
    ScimAdminGroupResponse:
      type: object
      required:
        - id
        - display_name
        - role_ids
      properties:
        display_name:
          type: string
        id:
          type: string
          format: uuid
        role_ids:
          type: array
          items:
            type: string
            format: uuid
    ScimConnectionResponse:
      type: object
      required:
        - id
        - display_name
        - client_id
        - active
        - version
        - client_secret_expires_at
      properties:
        active:
          type: boolean
        client_id:
          type: string
        client_secret_expires_at:
          type: string
        display_name:
          type: string
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
    ScimErrorBody:
      type: object
      required:
        - schemas
        - status
        - detail
      properties:
        detail:
          type: string
        schemas:
          type: array
          items:
            type: string
        scimType:
          type:
            - string
            - "null"
        status:
          type: string
    ScimGroupInput:
      type: object
      required:
        - displayName
      properties:
        displayName:
          type: string
        externalId:
          type:
            - string
            - "null"
        members:
          type: array
          items:
            $ref: '#/components/schemas/ScimMemberInput'
        schemas:
          type: array
          items:
            type: string
    ScimMemberInput:
      type: object
      required:
        - value
      properties:
        value:
          type: string
          format: uuid
    ScimPatchOperation:
      type: object
      required:
        - op
      properties:
        op:
          type: string
        path:
          type:
            - string
            - "null"
        value: {}
    ScimPatchRequest:
      type: object
      required:
        - schemas
        - Operations
      properties:
        Operations:
          type: array
          items:
            $ref: '#/components/schemas/ScimPatchOperation'
        schemas:
          type: array
          items:
            type: string
    ScimTokenRequest:
      type: object
      required:
        - grant_type
      properties:
        client_id:
          type:
            - string
            - "null"
        client_secret:
          type:
            - string
            - "null"
        grant_type:
          type: string
    ScimTokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
        expires_in:
          type: integer
          format: int64
        token_type:
          type: string
    ScimUserInput:
      type: object
      required:
        - userName
      properties:
        active:
          type: boolean
        displayName:
          type:
            - string
            - "null"
        externalId:
          type:
            - string
            - "null"
        schemas:
          type: array
          items:
            type: string
        userName:
          type: string
    SetIntegrationSyncRequest:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean
    SetRagConfigRequest:
      type: object
      description: Request to set a project's RAG configuration (`PUT .../rag-config`).
      required:
        - enabled
      properties:
        default_collection:
          type:
            - string
            - "null"
          description: Default vector collection for RAG operations.
        enabled:
          type: boolean
          description: Whether RAG should be enabled for this project.
    SparseEmbedding:
      type: object
      description: |
        Stub result type for builds without the `sparse-embedding-presets` feature.

        Field names match the real type so JSON round-trips remain schema-compatible.
      required:
        - indices
        - values
      properties:
        indices:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: Vocabulary token ids with non-zero weight, ascending.
        values:
          type: array
          items:
            type: number
            format: float
          description: Weights parallel to `indices`.
    StructuredData:
      type: object
      description: Structured data (Schema.org, microdata, RDFa) block.
      required:
        - data_type
        - raw_json
      properties:
        data_type:
          $ref: '#/components/schemas/StructuredDataType'
          description: Type of structured data
        raw_json:
          type: string
          description: Raw JSON string representation
        schema_type:
          type:
            - string
            - "null"
          description: Schema type if detectable (e.g., "Article", "Event", "Product")
    StructuredDataType:
      type: string
      description: Structured data type classification.
      enum:
        - json-ld
        - microdata
        - rdfa
    SummaryStrategy:
      type: string
      description: Summarisation strategy.
      enum:
        - extractive
        - abstractive
    Table:
      type: object
      description: |-
        Extracted table structure.

        Represents a table detected and extracted from a document (PDF, image, etc.).
        Tables are converted to both structured cell data and Markdown format.
      required:
        - cells
        - markdown
        - page_number
      properties:
        bounding_box:
          $ref: '#/components/schemas/BoundingBox'
          description: |-
            Bounding box of the table's position. Only populated when position data is
            available from the producing extractor.

            The coordinate space depends on how the table was produced, and callers must
            know which route produced a given `Table` before interpreting this field:

            - Tables extracted from a PDF's native content, and tables recognized on a
              scanned PDF page that went through xberg's OCR pipeline (`--force-ocr` /
              `--ocr-scanned-pages`), are in **PDF points with a bottom-left origin**
              (x0=left, y0=bottom, x1=right, y1=top; y increases upward). For the OCR
              case, the pipeline rescales the backend's raw pixel output into this space
              before it reaches `Table::bounding_box` — see
              `rescale_ocr_bboxes_to_page_points` in `extractors::pdf::ocr`.
            - Tables detected by OCR on a standalone image with no backing PDF page (for
              example extracting a bare PNG/JPEG/TIFF) are in **raster pixel coordinates
              with a top-left origin** (x0=left, y0=top, x1=right, y1=bottom; y increases
              downward) — the same convention the OCR backend (Tesseract, PaddleOCR,
              candle-based backends) or the layout detector reported them in. There is no
              PDF page geometry to rescale into for this case, so the raw pixel box is
              passed through unchanged.
        cell_styles:
          type: array
          items:
            $ref: '#/components/schemas/TableCellStyle'
          description: |-
            Paragraph styles carried by individual cells, for the cells that have one.

            Sparse and flat on purpose. A DOCX banner row -- row 0, one cell spanning the grid,
            styled `Heading1`..`Heading6` -- is what Word's navigation pane and a `TOC` field treat
            as the document outline, but as a table cell it reached consumers as anonymous text
            (GH#1587). `cells` keeps the bare text: prefixing it with `#` would put a markdown
            heading inside a table cell, which is invalid where it lands and would change text every
            existing consumer already reads. This list is the signal instead.

            Entries are only emitted for cells that actually carry a style, so an ordinary table
            serialises exactly as it did before. Indices are into `cells`.
        cells:
          type: array
          items:
            type: array
            items:
              type: string
          description: Table cells as a 2D vector (rows × columns)
        columns:
          type:
            - array
            - "null"
          items:
            type: string
          description: |-
            Header cells for this fragment, i.e. the first row of `cells`.

            Populated even when this fragment's own header row was merged away or
            physically lives in a sibling fragment (see `table_id`), so a single
            fragment is interpretable in isolation. `None` when no header row
            could be determined.
        markdown:
          type: string
          description: Markdown representation of the table
        page_number:
          type: integer
          format: int32
          description: Page number where the table was found (1-indexed)
          minimum: 0
        table_id:
          type:
            - string
            - "null"
          description: |-
            Stable identifier shared by every `tables[]` entry that represents a
            fragment of the same physical table.

            Assigned deterministically by the extraction pipeline (e.g. a
            sequential `"table-N"` in document order); never derived from
            randomness or wall-clock time, so the same input document always
            produces the same ids. Consumers can use it to reconcile the markdown
            blocks in `content` / `pages[].content` / `chunks[].content` with the
            structured entries in `tables[]`. `None` when the extractor did not
            assign one.

            Today, same-page fragments of one physical table are already merged
            into a single `tables[]` entry before ids are assigned (see PDF table
            stitching), so in practice `table_id` is unique per entry rather than
            shared across several. A table split across a page boundary is
            intentionally *not* linked — its per-page pieces get separate ids.
            Sharing one id across page-boundary fragments is a known possible
            future extension, not implemented yet.
    TableCellStyle:
      type: object
      description: |-
        The paragraph style a single table cell's text carries, located by grid position.

        Flat rather than a nested `Vec<Vec<Option<..>>>`: the nested shape marshals badly across the
        FFI bindings, and the data is sparse anyway. See `Table.cell_styles`.
      required:
        - row
        - col
      properties:
        col:
          type: integer
          format: int32
          description: Zero-indexed column of the cell this style belongs to.
          minimum: 0
        heading_level:
          type:
            - integer
            - "null"
          format: int32
          description: Outline level 1-6 when the style resolves to a heading, otherwise `None`.
          minimum: 0
        row:
          type: integer
          format: int32
          description: Zero-indexed row of the cell this style belongs to.
          minimum: 0
        style_name:
          type:
            - string
            - "null"
          description: Human-readable style name, e.g. `heading 2`.
    TableGrid:
      type: object
      description: |-
        Structured table grid with cell-level metadata.

        Stores row/column dimensions and a flat list of cells with position info.
      required:
        - rows
        - cols
        - cells
      properties:
        cells:
          type: array
          items:
            $ref: '#/components/schemas/GridCell'
          description: All cells in row-major order.
        cols:
          type: integer
          format: int32
          description: Number of columns in the table.
          minimum: 0
        rows:
          type: integer
          format: int32
          description: Number of rows in the table.
          minimum: 0
    TextAnnotation:
      type: object
      description: |-
        Inline text annotation — byte-range based formatting and links.

        Annotations reference byte offsets into the node's text content,
        enabling precise identification of formatted regions.
      required:
        - start
        - end
        - kind
      properties:
        end:
          type: integer
          format: int32
          description: End byte offset in the node's text content (exclusive).
          minimum: 0
        kind:
          $ref: '#/components/schemas/AnnotationKind'
          description: Annotation type.
        start:
          type: integer
          format: int32
          description: Start byte offset in the node's text content (inclusive).
          minimum: 0
    TextDirection:
      type: string
      description: Text direction enumeration for HTML documents.
      enum:
        - ltr
        - rtl
        - auto
    TextMetadata:
      type: object
      description: |-
        Text/Markdown metadata.

        Extracted from plain text and Markdown files. Includes word counts and,
        for Markdown, structural elements like headers and links.
      required:
        - line_count
        - word_count
        - character_count
      properties:
        character_count:
          type: integer
          format: int32
          description: Number of characters
          minimum: 0
        code_blocks:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/MarkdownCodeBlock'
          description: Code blocks (for Markdown files).
        headers:
          type:
            - array
            - "null"
          items:
            type: string
          description: Markdown headers (headings text only, for Markdown files)
        line_count:
          type: integer
          format: int32
          description: Number of lines in the document
          minimum: 0
        links:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/MarkdownLink'
          description: Markdown links (for Markdown files).
        word_count:
          type: integer
          format: int32
          description: Number of words
          minimum: 0
    Translation:
      type: object
      description: |-
        Translation of the extracted content.

        Holds the translated rendition of `ExtractedDocument::content` and (when
        `preserve_markup` was requested) the translated `formatted_content`. Chunks
        are translated in place inside `ExtractedDocument::chunks[*].content` rather
        than duplicated here.
      required:
        - target_lang
        - content
      properties:
        content:
          type: string
          description: Translated plain-text body. Matches the shape of `ExtractedDocument::content`.
        formatted_content:
          type:
            - string
            - "null"
          description: |-
            Translated markup body (Markdown / HTML / etc.) when `preserve_markup` was
            enabled on the config. `None` otherwise.
        source_lang:
          type:
            - string
            - "null"
          description: BCP-47 source language. `None` when the translation backend was asked to detect.
        target_lang:
          type: string
          description: BCP-47 language tag the translation was produced into (e.g. `"de"`, `"fr-CA"`).
    UpdateMemberRoleRequest:
      type: object
      description: Request to update a member's role within a project.
      required:
        - role
      properties:
        role:
          type: string
          description: 'New role: `"OWNER"` or `"MEMBER"`.'
    UpdateProjectRequest:
      type: object
      description: |-
        Request to update an existing project. Enterprise-only today (Pro has no
        project-update endpoint).
      properties:
        name:
          type:
            - string
            - "null"
          description: Updated project name.
    UpdateProjectRoleRequest:
      type: object
      required:
        - name
        - permissions
      properties:
        description:
          type:
            - string
            - "null"
        name:
          type: string
        permissions:
          type: array
          items:
            $ref: '#/components/schemas/ProjectPermission'
    UpdateUserProfileRequest:
      type: object
      description: Fields the authenticated user may change on their profile.
      required:
        - display_name
      properties:
        display_name:
          type: string
          description: Non-empty display name of at most 255 characters.
          example: Jane Doe
          maxLength: 255
      additionalProperties: false
    UpdateWebhookRequest:
      type: object
      description: |-
        Request to update a webhook. All fields optional; at least one must be
        set for the update to be meaningful.
      properties:
        events:
          type:
            - array
            - "null"
          items:
            type: string
          description: Updated event subscription list.
        is_active:
          type:
            - boolean
            - "null"
          description: Updated active status.
        name:
          type:
            - string
            - "null"
          description: Updated webhook name.
        url:
          type:
            - string
            - "null"
          description: Updated webhook URL.
    UriKind:
      type: string
      description: Semantic classification of an extracted URI.
      enum:
        - hyperlink
        - image
        - anchor
        - citation
        - reference
        - email
    UsageInfo:
      type: object
      description: Current usage for the billing period
      required:
        - pages_used
        - total_documents
        - total_failed
        - by_mime_type
      properties:
        by_mime_type:
          type: object
          description: Usage breakdown by MIME type
          additionalProperties:
            $ref: '#/components/schemas/MimeTypeUsage'
          propertyNames:
            type: string
        pages_remaining:
          type:
            - integer
            - "null"
          format: int64
          description: Pages remaining before hitting the limit (null = unlimited)
        pages_used:
          type: integer
          format: int64
          description: Total pages used in the current period
        total_documents:
          type: integer
          format: int32
          description: Total documents processed
        total_failed:
          type: integer
          format: int32
          description: Total failed documents
    UsagePeriod:
      type: object
      description: Period details in usage response
      required:
        - start_date
        - end_date
      properties:
        end_date:
          type: string
          description: End date (ISO 8601 format)
          example: 2025-01-31
        start_date:
          type: string
          description: Start date (ISO 8601 format)
          example: 2025-01-01
    UsageQuery:
      type: object
      description: Query parameters for usage statistics endpoint
      required:
        - start_date
        - end_date
      properties:
        end_date:
          type: string
          description: End date for the usage period (ISO 8601 format, e.g., "2025-01-31")
        start_date:
          type: string
          description: Start date for the usage period (ISO 8601 format, e.g., "2025-01-01")
    UsageResponse:
      type: object
      description: Response containing usage statistics
      required:
        - project_id
        - period
        - summary
        - by_method
        - by_status_code
        - by_path
        - error_details
      properties:
        by_method:
          type: object
          description: Request count by HTTP method
          additionalProperties:
            type: integer
            format: int64
          propertyNames:
            type: string
        by_path:
          type: object
          description: Request count by path (normalized)
          additionalProperties:
            type: integer
            format: int64
          propertyNames:
            type: string
        by_status_code:
          type: object
          description: Request count by status code
          additionalProperties:
            type: integer
            format: int64
          propertyNames:
            type: integer
            format: int32
        error_details:
          type: array
          items:
            $ref: '#/components/schemas/ErrorDetail'
          description: Recent error details (up to 100)
        period:
          $ref: '#/components/schemas/UsagePeriod'
          description: Time period covered by these statistics
        project_id:
          type: string
          description: The project identifier
          example: 550e8400-e29b-41d4-a716-446655440000
        summary:
          $ref: '#/components/schemas/UsageSummary'
          description: Aggregated statistics for the period
    UsageSummary:
      type: object
      description: Summary statistics for a usage period
      required:
        - total_requests
        - successful_requests
        - failed_requests
        - average_response_time_ms
        - total_request_bytes
        - total_response_bytes
      properties:
        average_response_time_ms:
          type: integer
          format: int32
          description: Average response time in milliseconds
          example: 125
        failed_requests:
          type: integer
          format: int64
          description: Number of failed requests (status >= 400)
          example: 50
        successful_requests:
          type: integer
          format: int64
          description: Number of successful requests (status < 400)
          example: 1450
        total_request_bytes:
          type: integer
          format: int64
          description: Total request size in bytes
          example: 1048576
        total_requests:
          type: integer
          format: int64
          description: Total number of requests
          example: 1500
        total_response_bytes:
          type: integer
          format: int64
          description: Total response size in bytes
          example: 2097152
    UserInfo:
      type: object
      description: User information in login response
      required:
        - user_id
      properties:
        default_project_id:
          type:
            - string
            - "null"
          format: uuid
          description: |-
            Default project ID (first OWNER project), or null when the user is signing in for
            the first time and has pending invitations they must accept before any project
            becomes their default.
          example: 550e8400-e29b-41d4-a716-446655440000
        email:
          type:
            - string
            - "null"
          description: User email address
          example: user@example.com
        name:
          type:
            - string
            - "null"
          description: User display name
          example: Jane Doe
        user_id:
          type: string
          description: User ID from identity provider
          example: user-id-abc123
    UserProfileResponse:
      type: object
      description: The authenticated user's durable profile.
      required:
        - user_id
      properties:
        display_name:
          type:
            - string
            - "null"
          description: User-chosen display name, initially seeded from OIDC claims.
          example: Jane Doe
        email:
          type:
            - string
            - "null"
          description: Most recently observed email from trusted OIDC claims.
          example: user@example.com
        user_id:
          type: string
          description: User ID from the identity provider.
          example: user-id-abc123
    WebhookDeliveryDetailResponse:
      type: object
      description: |-
        Diagnostic payload previews for one authorized delivery; eligible for cleanup after seven days.
        Previews are capped at 4096 UTF-8 bytes and can contain tenant document content.
      required:
        - delivery
        - request_preview
        - request_truncated
        - response_truncated
      properties:
        delivery:
          $ref: '#/components/schemas/WebhookDeliveryResponse'
        request_preview:
          type: string
        request_truncated:
          type: boolean
        response_error:
          type:
            - string
            - "null"
          description: A failure to capture the response body, separate from the HTTP delivery outcome.
        response_preview:
          type:
            - string
            - "null"
        response_truncated:
          type: boolean
    WebhookDeliveryResponse:
      type: object
      description: |-
        One past delivery attempt for a webhook subscription.

        **Payload bodies are deliberately not included here.** A delivery's
        request and response bodies can carry document extraction content — the
        same `results` payload `WebhookEvent` embeds — which is exactly the class
        of data `WEBHOOK_RESULTS_MAX_BYTES` already treats as too sensitive/large
        to always inline. A list endpoint that paginates delivery *metadata*
        should not also be the place a caller incidentally pages through
        potentially large document content: that argues for a separate
        `GET .../deliveries/{delivery_id}` detail endpoint fetched on demand (not
        declared by this pass — only the list and retry operations are), the same
        "small by default, fetch the rest separately" shape the job-result and
        webhook-event payloads already use.
      required:
        - id
        - event_id
        - event_type
        - job_id
        - attempt_number
        - success
        - duration_ms
        - occurred_at
      properties:
        attempt_number:
          type: integer
          format: int64
          description: |-
            1-based attempt number within the redelivery sequence for this event
            Automatic retries follow the configured delivery limit.
        completed_at:
          type:
            - string
            - "null"
          description: Completion time; absent if the sender was interrupted before recording an outcome.
        duration_ms:
          type: integer
          format: int64
          description: How long the delivery attempt took, in milliseconds.
        error:
          type:
            - string
            - "null"
          description: Error message, present when the attempt did not succeed.
        event_id:
          type: string
          format: uuid
          description: Stable event identifier shared by automatic redeliveries.
        event_type:
          type: string
          description: The event type delivered (e.g. `"job.completed"`, `"job.failed"`).
        id:
          type: string
          format: uuid
          description: Unique identifier of this delivery attempt.
        job_id:
          type: string
          description: The job this delivery attempt carries the event for.
        occurred_at:
          type: string
          description: ISO 8601 timestamp the attempt occurred at.
        status_code:
          type:
            - integer
            - "null"
          format: int32
          description: |-
            HTTP status code returned by the endpoint, when a response was
            received at all (absent on a connection/timeout failure).
          minimum: 0
        success:
          type: boolean
          description: Whether the endpoint accepted the delivery.
    WebhookResponse:
      type: object
      description: Webhook resource details.
      required:
        - id
        - name
        - url
        - events
        - is_active
        - total_deliveries
        - failed_deliveries
        - created_at
        - updated_at
      properties:
        created_at:
          type: string
          description: ISO 8601 creation timestamp.
        events:
          type: array
          items:
            type: string
          description: Subscribed event types (e.g. `"job.completed"`, `"job.failed"`).
        failed_deliveries:
          type: integer
          format: int32
          description: Number of failed delivery attempts.
        id:
          type: string
          format: uuid
          description: Unique webhook identifier.
        is_active:
          type: boolean
          description: Whether the webhook is currently active.
        last_delivery_at:
          type:
            - string
            - "null"
          description: ISO 8601 timestamp of the last delivery attempt.
        last_delivery_status:
          type:
            - string
            - "null"
          description: Status of the last delivery attempt (e.g. `"success"`, `"failed"`).
        name:
          type: string
          description: Webhook display name.
        total_deliveries:
          type: integer
          format: int32
          description: Total number of delivery attempts.
        updated_at:
          type: string
          description: ISO 8601 last update timestamp.
        url:
          type: string
          description: Webhook delivery URL.
    WebhookTestResponse:
      type: object
      description: Result of a manual test delivery to a webhook endpoint.
      required:
        - success
      properties:
        error:
          type:
            - string
            - "null"
          description: Error message, when the delivery failed.
        status_code:
          type:
            - integer
            - "null"
          format: int32
          description: HTTP status code returned by the webhook endpoint, when reached.
          minimum: 0
        success:
          type: boolean
          description: Whether the test delivery was successful.
    XmlMetadata:
      type: object
      description: |-
        XML metadata extracted during XML parsing.

        Provides statistics about XML document structure.
      required:
        - element_count
        - unique_elements
      properties:
        element_count:
          type: integer
          format: int32
          description: Total number of XML elements processed
          minimum: 0
        unique_elements:
          type: array
          items:
            type: string
          description: List of unique element tag names (sorted)
    YearRange:
      type: object
      description: Year range for bibliographic metadata.
      properties:
        max:
          type:
            - integer
            - "null"
          format: int32
          description: Latest (maximum) year in the range.
          minimum: 0
        min:
          type:
            - integer
            - "null"
          format: int32
          description: Earliest (minimum) year in the range.
          minimum: 0
        years:
          type: array
          items:
            type: integer
            format: int32
            minimum: 0
          description: All individual years present in the collection.
  securitySchemes:
    bearer_auth:
      type: http
      scheme: bearer
tags:
  - name: health
    description: Health check endpoints
  - name: auth
    description: Authentication endpoints
  - name: projects
    description: Project management
  - name: members
    description: Project members management
  - name: api_keys
    description: API key management
  - name: scim
    description: SCIM directory provisioning configuration
  - name: audit
    description: Audit log
  - name: integrations
    description: Integration management (OAuth2/OIDC)
  - name: oauth
    description: OAuth2/OIDC callback processing
  - name: webhooks
    description: Webhook management
  - name: usage
    description: Usage statistics
  - name: analytics
    description: Rich analytics data
  - name: sandbox
    description: Sandbox extraction playground
  - name: billing
    description: Billing and quota information
