openapi: 3.1.0
info:
  title: Klarefi API
  version: "1.0"
  description: |
    Public machine-to-machine API for Klarefi: hosted intake sessions,
    document processing, case files, GDPR erasure, and webhook management.

    Authentication is a Bearer API key (`sk_live_...` or `sk_test_...`)
    minted in the dashboard Developer tab. Each key carries scopes; the
    required scope is listed per operation. Live self-serve keys require active
    billing and current legal acceptance; otherwise authenticated requests may
    return `402 billing_required` or `428 legal_acceptance_required`.

    Outbound webhooks are signed with HMAC-SHA256 over
    `{timestamp}.{rawBody}` and delivered with the header
    `X-Klarefi-Signature: t={timestamp},v1={hexDigest}`, retried on an
    exponential backoff schedule (60s, 5m, 30m, 2h; max 5 attempts).
servers:
  - url: https://www.klarefi.com
    description: Canonical production API base URL.
  - url: https://{deployment}.convex.site
    description: Advanced direct-deployment URL shown in Settings → Developer.
    variables:
      deployment:
        default: your-deployment
        description: Convex deployment that serves your Klarefi org.
security:
  - apiKey: []
tags:
  - name: sessions
  - name: cases
  - name: operator
  - name: workflows
  - name: connectors
  - name: documents
  - name: webhooks
  - name: meta

paths:
  /api/v1/health:
    get:
      tags: [meta]
      summary: Health check
      security: []
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ok

  /api/v1/me:
    get:
      tags: [meta]
      summary: Identify the org behind the API key
      description: Requires the `cases:read` scope.
      responses:
        "200":
          description: Org identity.
          content:
            application/json:
              schema:
                type: object
                properties:
                  org_id:
                    type: string
                  environment:
                    $ref: "#/components/schemas/Environment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/sessions:
    post:
      tags: [sessions]
      summary: Create a hosted intake session
      description: |
        Stripe Checkout pattern: create a session server-side, then redirect
        the applicant to `signed_url`. Requires the `intake:sessions:create`
        scope. Idempotent on `idempotency_key`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionCreateParams"
      responses:
        "201":
          description: Session created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionCreateResponse"
        "200":
          description: Idempotent replay of a previous creation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionCreateResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "402":
          $ref: "#/components/responses/BillingRequired"
        "428":
          $ref: "#/components/responses/PreconditionRequired"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/sessions/{sessionId}/handoff:
    post:
      tags: [sessions]
      summary: Complete the customer-side handoff
      description: |
        Marks the customer-prepared portion of a hosted intake complete and
        returns a fresh applicant link and bearer token. Requires the
        `intake:sessions:create` scope. An optional notification email is
        fail-soft and is reported in the response.
      parameters:
        - $ref: "#/components/parameters/sessionId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionHandoffParams"
      responses:
        "200":
          description: Handoff marked or replayed with fresh applicant credentials.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionHandoffResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The session is terminal or cannot be handed off.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/process:
    post:
      tags: [cases]
      summary: Submit a document URL for processing
      description: |
        Creates a case and queues document processing. Requires the
        `cases:process` scope. Idempotent on `idempotency_key`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [document_url, case_type_id]
              properties:
                document_url:
                  type: string
                  format: uri
                  description: Public HTTPS URL of the document.
                case_type_id:
                  type: string
                external_case_id:
                  type: string
                external_applicant_id:
                  type: string
                external_customer_id:
                  type: string
                idempotency_key:
                  type: string
                  description: May also be sent as the Idempotency-Key header.
                trace_id:
                  type: string
      responses:
        "201":
          description: Document queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  case_id: { type: string }
                  doc_id: { type: string }
                  job_id: { type: string }
                  status: { type: string, const: queued }
                  idempotent_replay: { type: boolean }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "402":
          $ref: "#/components/responses/BillingRequired"
        "428":
          $ref: "#/components/responses/PreconditionRequired"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/connectors:
    get:
      tags: [connectors]
      summary: List configured connectors
      description: |
        Returns the org's configured connectors with secrets redacted.
        Requires `connectors:read` or `connectors:write` (write implies read).
      responses:
        "200":
          description: Connector list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [connectors]
      summary: Create or update a connector
      description: |
        Upserts a connector by `connector_key`. The connector is active as soon
        as this call returns. There is no draft or review lifecycle, so review
        its functions and credentials before routing live traffic. Requires the
        `connectors:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectorUpsertParams"
      responses:
        "201":
          description: Connector created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorUpsertResponse"
        "200":
          description: Existing connector updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorUpsertResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/connectors/import:
    post:
      tags: [connectors]
      summary: Import a connector from an OpenAPI document
      description: |
        Generates a connector from an OpenAPI document supplied inline
        (`openapi_json` string or `openapi` object) or fetched from a public
        `openapi_url`. JSON only; YAML specs are rejected with
        `openapi_yaml_not_supported` (convert to JSON first). The imported
        connector goes live immediately. Review its generated functions before
        routing live traffic. Requires the `connectors:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectorImportParams"
      responses:
        "201":
          description: Connector created from the spec.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorImportResponse"
        "200":
          description: Existing connector updated from the spec.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorImportResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/connectors/{connectorKey}:
    get:
      tags: [connectors]
      summary: Get a connector by key
      description: |
        Returns a single connector with secrets redacted. Requires
        `connectors:read` or `connectors:write` (write implies read).
      parameters:
        - $ref: "#/components/parameters/connectorKey"
      responses:
        "200":
          description: Connector detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorGetResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      tags: [connectors]
      summary: Delete a connector by key
      description: Requires the `connectors:write` scope.
      parameters:
        - $ref: "#/components/parameters/connectorKey"
      responses:
        "200":
          description: Connector deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorDeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/cases/{caseId}:
    get:
      tags: [cases]
      summary: Case status and resolved facts
      description: Requires the `cases:read` scope.
      parameters:
        - $ref: "#/components/parameters/caseId"
      responses:
        "200":
          description: Case status and facts.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/cases/{caseId}/events:
    get:
      tags: [cases]
      summary: Cursor-paginated case intake events
      description: Requires the `cases:read` scope.
      parameters:
        - $ref: "#/components/parameters/caseId"
        - name: after_cursor
          in: query
          schema:
            type: integer
            default: 0
        - name: limit
          in: query
          description: Maximum events to return. Defaults to 100.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
      responses:
        "200":
          description: Events after the cursor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  case_id: { type: string }
                  events:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  next_cursor: { type: integer }
                  has_more: { type: boolean }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/cases/{caseId}/package:
    get:
      tags: [cases]
      summary: Decision-ready handoff package
      description: |
        The assembled case package plus `case_file_url`, a signed read-only
        link to the case file. Requires the `cases:read` scope.
      parameters:
        - $ref: "#/components/parameters/caseId"
      responses:
        "200":
          description: Handoff package.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  case_file_url:
                    type: [string, "null"]
                    format: uri
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/cases/{caseId}/workspace:
    get:
      tags: [operator]
      summary: Cited operator workspace
      description: Requires `cases:read`. Returns facts, evidence, blockers, ownership, and timeline.
      parameters:
        - $ref: "#/components/parameters/caseId"
      responses:
        "200":
          description: Operator workspace projection.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/cases/{caseId}/commands:
    post:
      tags: [operator]
      summary: Submit an intake-review command
      description: |
        Requires `cases:review`. API keys can review/correct facts, request
        clarification, update evidence, or escalate. They cannot complete or
        reopen cases and are recorded as `actor_kind: api_key`. An idempotency
        key is required in either the JSON body or the `Idempotency-Key` header.
      parameters:
        - $ref: "#/components/parameters/caseId"
        - name: Idempotency-Key
          in: header
          required: false
          description: Retry-safe key. Required unless `idempotency_key` is present in the JSON body.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [command_type, payload]
              properties:
                command_type:
                  type: string
                  enum:
                    [
                      approve_fact,
                      correct_fact,
                      request_applicant_info,
                      add_custom_evidence,
                      update_fact_evidence,
                      escalate,
                    ]
                payload: { type: object, additionalProperties: true }
                rationale: { type: string }
                idempotency_key:
                  type: string
                  description: Retry-safe key. Required unless supplied in the `Idempotency-Key` header.
                trace_id: { type: string }
      responses:
        "202":
          description: Command accepted for Python-owned execution.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperatorCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/cases/{caseId}/commands/{commandId}:
    get:
      tags: [operator]
      summary: Poll an intake-review command
      description: Requires `cases:read`.
      parameters:
        - $ref: "#/components/parameters/caseId"
        - name: commandId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Current command record.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperatorCommand" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/operator/queue:
    get:
      tags: [operator]
      summary: List the operator queue
      description: Requires `cases:review`. Results are restricted to the API key environment.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        "200":
          description: Bounded queue window.
          content:
            application/json:
              schema:
                type: object
                properties:
                  cases:
                    type: array
                    items: { type: object, additionalProperties: true }
                  environment: { $ref: "#/components/schemas/Environment" }

  /api/v1/workflows:
    get:
      tags: [workflows]
      summary: List workflow declarations
      description: Requires `workflows:read`.
      responses:
        "200":
          description: Versioned workflow declarations.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
    post:
      tags: [workflows]
      summary: Validate and save a workflow draft
      description: Requires `workflows:write`. Always saves as draft; activation remains human-controlled.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [workflow]
              properties:
                workflow: { type: object, additionalProperties: true }
      responses:
        "201":
          description: Draft saved; publishing is still required.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        "400": { $ref: "#/components/responses/ValidationError" }

  /api/v1/documents/{docId}:
    delete:
      tags: [documents]
      summary: GDPR erasure of a document
      description: Requires the `privacy:erase` scope.
      parameters:
        - name: docId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Erasure receipt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  erasure_receipt:
                    type: object
                    additionalProperties: true
        "202":
          description: Erasure batch accepted; more derived data remains.
          content:
            application/json:
              schema:
                type: object
                properties:
                  erasure_receipt:
                    type: object
                    additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/webhooks/test:
    post:
      tags: [webhooks]
      summary: Send a signed test delivery to an endpoint
      description: Requires the `webhooks:test` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [endpoint_url, signing_secret]
              properties:
                endpoint_url:
                  type: string
                  format: uri
                signing_secret:
                  type: string
      responses:
        "200":
          description: Test delivery result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookTestResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/webhooks/deliveries/{deliveryId}/ack:
    post:
      tags: [webhooks]
      summary: Acknowledge a webhook delivery
      description: |
        Confirms receipt of a delivery and advances the case handoff state
        machine. Requires the `webhooks:acknowledge` scope. Idempotent on
        `acknowledgement_id`.
      parameters:
        - name: deliveryId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                acknowledgement_id:
                  type: string
      responses:
        "200":
          description: Acknowledgement recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookAcknowledgeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: API key from the dashboard Developer tab.

  parameters:
    sessionId:
      name: sessionId
      in: path
      required: true
      schema:
        type: string
    caseId:
      name: caseId
      in: path
      required: true
      schema:
        type: string
    connectorKey:
      name: connectorKey
      in: path
      required: true
      description: Connector key (lowercase letters, numbers, underscores).
      schema:
        type: string

  schemas:
    Environment:
      type: string
      enum: [live, test]

    SessionCreateParams:
      type: object
      required: [case_type_id]
      properties:
        case_type_id:
          type: string
        ttl_hours:
          type: number
          default: 168
        return_url:
          type: string
          format: uri
        locale:
          type: string
        external_case_id:
          type: string
        external_applicant_id:
          type: string
        external_customer_id:
          type: string
        idempotency_key:
          type: string
          description: May also be sent as the Idempotency-Key header.
        trace_id:
          type: string
        prefill:
          type: object
          required: [field_values]
          properties:
            field_values:
              type: object
              additionalProperties:
                type: [string, "null"]
              description: Initial form values keyed by configured field id.

    SessionCreateResponse:
      type: object
      required:
        - session_id
        - signed_url
        - access_token
        - access_token_expires_at
        - expires_at
        - agent
        - snippet
        - idempotent_replay
      properties:
        session_id:
          type: string
        signed_url:
          type: string
          format: uri
          description: Hosted intake URL with embedded signed token.
        expires_at:
          type: string
          format: date-time
        access_token:
          type: string
          description: Per-session bearer token for applicant-plane calls.
        access_token_expires_at:
          type: string
          format: date-time
        prefill:
          type: object
          required: [applied_field_ids]
          properties:
            applied_field_ids:
              type: array
              items: { type: string }
        prefill_ignored_on_replay:
          type: boolean
          description: True when prefill was ignored on an idempotent replay.
        agent:
          type: object
          required: [api_base, manifest_url, access_token]
          properties:
            api_base:
              type: string
              format: uri
              description: Correct applicant API base for this deployment.
            manifest_url:
              type: string
              format: uri
            access_token:
              type: string
        snippet:
          type: string
          description: Drop-in script tag that redirects to the signed URL.
        idempotent_replay:
          type: boolean

    SessionHandoffParams:
      type: object
      properties:
        notify:
          type: object
          required: [email]
          properties:
            email:
              type: string
              format: email

    SessionHandoffResponse:
      type: object
      required:
        - session_id
        - signed_url
        - access_token
        - access_token_expires_at
        - expires_at
        - customer_completed_at
        - already_marked
      properties:
        session_id: { type: string }
        signed_url:
          type: string
          format: uri
        access_token:
          type: string
        access_token_expires_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        customer_completed_at:
          type: string
          format: date-time
        already_marked: { type: boolean }
        notification:
          type: object
          required: [requested, sent]
          properties:
            requested:
              type: boolean
              const: true
            sent: { type: boolean }
            reason:
              type: string
              enum: [email_not_configured, send_failed]

    KeyValuePair:
      type: object
      required: [key, value]
      properties:
        key: { type: string }
        value: { type: string }

    ConnectorAuth:
      type: object
      description: |
        Outbound auth for the connector transport. Secrets are write-only; they
        are never returned in list/get responses (redacted to a has_secret flag).
      required: [mode]
      properties:
        mode:
          type: string
          enum:
            - none
            - bearer_token
            - basic_auth
            - api_key_header
            - api_key_query
        header_name:
          type: string
          description: Required when mode is api_key_header.
        query_name:
          type: string
          description: Required when mode is api_key_query.
        secret:
          type: string
          description: Required for bearer_token, api_key_header, api_key_query.
        username:
          type: string
          description: Required when mode is basic_auth.
        password:
          type: string
          description: Required when mode is basic_auth.

    ConnectorFunction:
      type: object
      required:
        [function_key, label, usage, request, sample_input, success_criteria]
      properties:
        function_key:
          type: string
          description: Lowercase letters, numbers, and underscores.
        label: { type: string }
        description: { type: string }
        usage:
          type: string
          const: verify_fact
        request:
          type: object
          required: [method, path]
          properties:
            method:
              type: string
              enum: [GET, POST, PUT, PATCH, DELETE]
            path:
              type: string
              description: Path template appended to the transport base URL.
            headers:
              type: array
              items:
                $ref: "#/components/schemas/KeyValuePair"
            query:
              type: array
              items:
                $ref: "#/components/schemas/KeyValuePair"
            body_template:
              type: string
        input_parameters:
          type: array
          items:
            type: object
            required: [name, type, required]
            properties:
              name: { type: string }
              type:
                type: string
                enum: [text, number, boolean, date]
              required: { type: boolean }
              description: { type: string }
        sample_input:
          type: object
          additionalProperties:
            type: [string, number, boolean]
        success_criteria:
          type: object
          required: [status_codes]
          properties:
            status_codes:
              type: array
              items: { type: integer }
              minItems: 1
              maxItems: 10
            expected_content_type: { type: string }
            json_path_presence:
              type: array
              items: { type: string }
            response_assertions:
              type: array
              items:
                type: object
                additionalProperties: true

    ConnectorUpsertParams:
      type: object
      required: [connector_key, label, connector_type, transport, functions]
      properties:
        connector_key:
          type: string
          description: Lowercase letters, numbers, and underscores (max 64).
        label: { type: string }
        connector_type:
          type: string
          const: http_api
        transport:
          type: object
          required: [base_url, timeout_ms, auth]
          properties:
            base_url:
              type: string
              format: uri
              description: Public HTTPS base URL.
            default_headers:
              type: array
              items:
                $ref: "#/components/schemas/KeyValuePair"
            timeout_ms:
              type: integer
              minimum: 1000
              maximum: 30000
            auth:
              $ref: "#/components/schemas/ConnectorAuth"
        functions:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ConnectorFunction"

    ConnectorUpsertResponse:
      type: object
      properties:
        connector_key: { type: string }
        created: { type: boolean }
        environment:
          $ref: "#/components/schemas/Environment"

    ConnectorImportParams:
      type: object
      required: [connector_key]
      description: |
        Provide exactly one spec source: openapi_url, openapi_json, or openapi.
      properties:
        connector_key: { type: string }
        label: { type: string }
        base_url:
          type: string
          format: uri
          description: Overrides the server URL from the spec.
        openapi_url:
          type: string
          format: uri
          description: Public HTTPS URL that serves the OpenAPI JSON document.
        openapi_json:
          type: string
          description: OpenAPI document as a JSON string. JSON only, not YAML.
        openapi:
          type: object
          additionalProperties: true
          description: OpenAPI document as an inline JSON object.
        auth:
          $ref: "#/components/schemas/ConnectorAuth"

    ConnectorImportResponse:
      type: object
      properties:
        connector_key: { type: string }
        created: { type: boolean }
        environment:
          $ref: "#/components/schemas/Environment"
        functions_imported: { type: integer }

    ConnectorListResponse:
      type: object
      properties:
        connectors:
          type: array
          items:
            type: object
            additionalProperties: true
            description: Configured connector with secrets redacted.
        environment:
          $ref: "#/components/schemas/Environment"

    ConnectorGetResponse:
      type: object
      properties:
        connector:
          type: object
          additionalProperties: true
          description: Configured connector with secrets redacted.
        environment:
          $ref: "#/components/schemas/Environment"

    ConnectorDeleteResponse:
      type: object
      properties:
        connector_key: { type: string }
        deleted: { type: boolean }
        environment:
          $ref: "#/components/schemas/Environment"

    WebhookEvent:
      type: object
      description: Payload delivered to registered webhook endpoints.
      properties:
        event_id: { type: string }
        delivery_id: { type: string }
        event_type:
          type: string
          description: Catalog event, e.g. v1.intake.submitted.
        semantic_event_type: { type: string }
        schema_version: { type: string }
        created_at: { type: string, format: date-time }
        occurred_at: { type: string, format: date-time }
        org_id: { type: string }
        case_id: { type: [string, "null"] }
        session_id: { type: [string, "null"] }
        external_case_id: { type: [string, "null"] }
        external_applicant_id: { type: [string, "null"] }
        external_customer_id: { type: [string, "null"] }
        idempotency_key: { type: [string, "null"] }
        trace_id: { type: [string, "null"] }
        data:
          type: object
          additionalProperties: true

    OperatorCommand:
      type: object
      required: [command_id, status, updated_at]
      properties:
        command_id: { type: string }
        job_id: { type: [string, "null"] }
        status:
          type: string
          enum: [accepted, applied, rejected, failed]
        updated_at: { type: string, format: date-time }
      additionalProperties: true

    WebhookTestResponse:
      type: object
      required: [success, status_code]
      properties:
        success:
          type: boolean
        status_code:
          type: [integer, "null"]
        error:
          type: string

    WebhookAcknowledgeResponse:
      type: object
      required: [acknowledged, already_acknowledged, delivery_id]
      properties:
        acknowledged:
          type: boolean
        already_acknowledged:
          type: boolean
        delivery_id:
          type: string
        case_id:
          type: string
        handoff_state:
          type: string

    ApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum:
                - authentication_error
                - authorization_error
                - validation_error
                - not_found
                - rate_limit_error
                - precondition_error
                - billing_error
                - internal_error
            code:
              type: string
              description: |
                Stable machine-readable error code. Current codes:
                `invalid_api_key`, `forbidden`, `billing_required`,
                `live_usage_cap_exceeded`, `precondition_required`,
                `legal_acceptance_required`, `self_serve_webhooks_disabled`,
                `rate_limit_exceeded`, `invalid_request`, `invalid_json`,
                `invalid_connector_payload`, `missing_connector_key`,
                `openapi_fetch_failed`, `missing_openapi_spec`,
                `openapi_yaml_not_supported`, `invalid_openapi_spec`,
                `invalid_openapi_connector`, `missing_document_url`,
                `invalid_document_url`,
                `per_request_webhook_url_not_supported`,
                `missing_case_type_id`, `invalid_integration_identity`,
                `missing_idempotency_key`, `idempotency_key_conflict`,
                `external_case_id_conflict`, `missing_case_id`,
                `invalid_after_cursor`, `invalid_limit`, `missing_doc_id`,
                `invalid_acknowledgement_payload`,
                `webhook_delivery_not_delivered`, `missing_endpoint_url`,
                `missing_signing_secret`, `invalid_endpoint_url`,
                `not_found`, `internal_error`.
            message: { type: string }
            request_id: { type: string }

  responses:
    Unauthorized:
      description: Missing, invalid, expired, revoked, or wrong-environment API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    Forbidden:
      description: API key is valid but lacks the required scope.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    ValidationError:
      description: Invalid request body or parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    RateLimited:
      description: Per-key rate limit exceeded. Check X-RateLimit-* headers.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    BillingRequired:
      description: Active billing is required for live API access.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
    PreconditionRequired:
      description: Current self-serve legal terms must be accepted before live API access.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
