openapi: 3.0.3
info:
  title: Integration Toolkit API
  version: 1.22.0
  description: API for integrating with external systems in a standardised way.
tags:
- name: erp
  description: ERP integration endpoints
- name: erp-imports
  description: ERP pricing file import endpoints
- name: trigger
  description: Endpoints to trigger ERP related actions
- name: integrations
  description: Integration and Use Case management endpoints
- name: monitoring
  description: Monitoring and analytics endpoints
- name: managed-call
  description: Managed call endpoints for synchronous external API calls
- name: proxy
  description: Secure proxy endpoints
servers:
- url: https://integration-toolkit.sls.epilot.io
- url: https://integration-toolkit.sls.epilot.io
security:
- EpilotAuth: []
paths:
  /v1/erp/tracking/acknowledgement:
    post:
      operationId: acknowledgeTracking
      summary: acknowledgeTracking
      description: Acknowledges an ERP tracking record by removing it from the tracking table, requires public authentication
      tags:
      - erp
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - ack_id
              properties:
                ack_id:
                  type: string
                  description: Unique identifier of the ERP tracking record to acknowledge
      responses:
        '200':
          description: Acknowledgment successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '400':
          description: Bad request - missing ack_id
        '401':
          description: Unauthorized - invalid or missing token
        '404':
          description: Record not found
        '500':
          description: Server error processing the request
  /v1/erp/trigger:
    post:
      operationId: triggerErp
      summary: triggerErp
      description: Triggers the ERP integration process
      tags:
      - erp
      - trigger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriggerErpActionRequest'
      responses:
        '200':
          $ref: '#/components/responses/TriggerWebhookResponse'
        '400':
          description: Bad request - invalid or missing parameters
        '401':
          description: Unauthorized - invalid or missing token
        '500':
          description: Server error processing the request
  /v1/erp/updates/events:
    post:
      deprecated: true
      operationId: processErpUpdatesEvents
      summary: processErpUpdatesEvents
      description: Handles updates from ERP systems and tracks them appropriately
      tags:
      - erp
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - app_id
              - component_id
              - meta
              - events
              properties:
                app_id:
                  type: string
                  format: uuid
                  description: UUID that identifies the specific integration app instance
                component_id:
                  type: string
                  format: uuid
                  description: UUID that identifies the specific integration app component instance
                meta:
                  type: object
                  description: Metadata to be passed along with the events
                  additionalProperties: true
                  properties:
                    correlation_id:
                      type: string
                      description: ID that identifies the specific request for debugging purposes
                events:
                  type: array
                  description: List of ERP events to process
                  items:
                    $ref: '#/components/schemas/ErpEvent'
            example:
              app_id: 123e4567-e89b-12d3-a456-426614174000
              component_id: 123e4567-e89b-12d3-a456-426614174000
              meta:
                correlation_id: req-789e4567-e89b-12d3-a456-426614174000
              events:
              - event_type: CREATE
                object_type: business_partner
                timestamp: '2025-05-01T08:30:00Z'
                format: json
                payload: '{"id":"BP10001","name":"Acme Corporation","type":"organization","tax_id":"DE123456789","status":"active"}'
                deduplication_id: bp-create-20250501-083000-001
              - event_type: UPDATE
                object_type: contract_account
                timestamp: '2025-05-01T08:35:00Z'
                format: json
                payload: '{"id":"CA20001","business_partner_id":"BP10001","status":"active","payment_method":"direct_debit"}'
                deduplication_id: ca-update-20250501-083500-001
      responses:
        '200':
          $ref: '#/components/responses/ERPUpdatesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/updates/events:
    post:
      deprecated: true
      operationId: processErpUpdatesEventsV2
      summary: processErpUpdatesEventsV2
      description: |
        Handles updates from ERP systems using integration_id directly.
        This is the v2 version that simplifies the API by accepting integration_id
        instead of app_id and component_id.
      tags:
      - erp
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErpUpdatesEventsV2Request'
            example:
              integration_id: 123e4567-e89b-12d3-a456-426614174000
              correlation_id: req-789e4567-e89b-12d3-a456-426614174000
              events:
              - event_type: CREATE
                object_type: business_partner
                timestamp: '2025-05-01T08:30:00Z'
                format: json
                payload: '{"id":"BP10001","name":"Acme Corporation","type":"organization","tax_id":"DE123456789","status":"active"}'
                deduplication_id: bp-create-20250501-083000-001
              - event_type: UPDATE
                object_type: contract_account
                timestamp: '2025-05-01T08:35:00Z'
                format: json
                payload: '{"id":"CA20001","business_partner_id":"BP10001","status":"active","payment_method":"direct_debit"}'
                deduplication_id: ca-update-20250501-083500-001
      responses:
        '200':
          $ref: '#/components/responses/ERPUpdatesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v3/erp/updates/events:
    post:
      operationId: processErpUpdatesEventsV3
      summary: processErpUpdatesEventsV3
      description: |
        Handles updates from ERP systems using integration_id directly.
        This is the v3 version that removes the unused event_type field and renames object_type to event_name
        to align with the integration UI naming.
      tags:
      - erp
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErpUpdatesEventsV3Request'
            example:
              integration_id: 123e4567-e89b-12d3-a456-426614174000
              correlation_id: req-789e4567-e89b-12d3-a456-426614174000
              events:
              - event_name: business_partner
                timestamp: '2025-05-01T08:30:00Z'
                format: json
                payload: '{"id":"BP10001","name":"Acme Corporation","type":"organization","tax_id":"DE123456789","status":"active"}'
                deduplication_id: bp-create-20250501-083000-001
              - event_name: contract_account
                timestamp: '2025-05-01T08:35:00Z'
                format: json
                payload: '{"id":"CA20001","business_partner_id":"BP10001","status":"active","payment_method":"direct_debit"}'
                deduplication_id: ca-update-20250501-083500-001
      responses:
        '200':
          $ref: '#/components/responses/ERPUpdatesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/updates/mapping_simulation:
    post:
      operationId: simulateMappingV2
      summary: simulateMappingV2
      description: |
        Test v2.0 mapping configuration by transforming a payload using the provided mapping rules without persisting data.

        This endpoint accepts the same configuration format that is stored in the integration use case resource,
        making it easier to test configurations before saving them to a use case.

        See documentation at /docs/MAPPING_V2.md for detailed v2.0 format specification.
      tags:
      - erp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MappingSimulationV2Request'
            examples:
              simple:
                summary: Simple Example
                value:
                  event_configuration:
                    entities:
                    - entity_schema: contact
                      unique_ids:
                      - customer_number
                      fields:
                      - attribute: customer_number
                        field: customerId
                      - attribute: first_name
                        field: firstName
                      - attribute: last_name
                        field: lastName
                      - attribute: email
                        field: email
                      - attribute: source
                        constant: ERP
                  format: json
                  payload: '{"customerId":"12345","firstName":"Anna","lastName":"Schmidt","email":"anna@example.com"}'
              advanced_with_relations:
                summary: Advanced with Relations
                value:
                  event_configuration:
                    entities:
                    - entity_schema: contract
                      unique_ids:
                      - contract_number
                      fields:
                      - attribute: contract_number
                        field: number
                      - attribute: display_name
                        field: displayName
                      - attribute: billing_account
                        relations:
                          operation: _set
                          items:
                          - entity_schema: billing_account
                            _tags:
                            - PRIMARY
                            unique_ids:
                            - attribute: external_id
                              field: customerAccountId
                    - entity_schema: meter
                      unique_ids:
                      - meter_number
                      jsonataExpression: 'meters.{"number": number, "type": meterType}'
                      fields:
                      - attribute: meter_number
                        field: number
                      - attribute: meter_type
                        field: type
                  format: json
                  payload: '{"number":"CTR-001","displayName":"Premium Contract","customerAccountId":"ACC-999","meters":[{"number":"M-001","meterType":"electricity"},{"number":"M-002","meterType":"gas"}]}'
              with_meter_readings:
                summary: With Meter Readings
                value:
                  event_configuration:
                    entities:
                    - entity_schema: contract
                      unique_ids:
                      - contract_number
                      fields:
                      - attribute: contract_number
                        field: number
                      - attribute: display_name
                        field: name
                    meter_readings:
                    - jsonataExpression: $.readings
                      meter:
                        unique_ids:
                        - attribute: external_id
                          field: meter_id
                      fields:
                      - attribute: external_id
                        field: reading_id
                      - attribute: timestamp
                        field: read_at
                      - attribute: source
                        constant: ERP
                      - attribute: value
                        field: value
                  format: json
                  payload: '{"number":"CTR-001","name":"Main Contract","readings":[{"meter_id":"M-001","reading_id":"R-001","read_at":"2025-01-15T10:00:00Z","value":12345.6}]}'
              conditional_fields:
                summary: Conditional Field Processing
                description: Example showing how to conditionally enable/disable individual field mappings
                value:
                  event_configuration:
                    entities:
                    - entity_schema: contact
                      unique_ids:
                      - customer_number
                      fields:
                      - attribute: customer_number
                        field: customerId
                      - attribute: email
                        field: email
                        enabled: $exists(email) and email != ''
                      - attribute: phone
                        field: phone
                        enabled: $exists(phone) and phone != ''
                      - attribute: vip_status
                        constant: VIP
                        enabled: vipFlag = true
                      - attribute: billing_account
                        enabled: $exists(billingAccountId)
                        relations:
                          operation: _set
                          items:
                          - entity_schema: billing_account
                            unique_ids:
                            - attribute: external_id
                              field: billingAccountId
                  format: json
                  payload: '{"customerId":"12345","email":"user@example.com","phone":"","vipFlag":true,"billingAccountId":"ACC-999"}'
      responses:
        '200':
          description: Successfully simulated mapping
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MappingSimulationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Unprocessable entity - mapping configuration or payload validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/erp/updates/direct_simulation:
    post:
      operationId: simulateDirect
      summary: simulateDirect
      description: |
        Dry run for direct-mode payloads: validates a `DirectPayload` against a `direct: true`
        use case configuration and previews the internal entity and meter reading updates the
        pipeline would apply — without persisting anything.

        Unlike the ingest endpoint, validation does not stop at the first problem: ALL issues
        across all operations are collected into `errors`, each carrying the index of the
        operation it refers to. When the payload is valid, the response additionally contains
        the translated update previews plus non-blocking `warnings` (for example unique-id
        fields that are not defined in the target entity schema, or a failed server-side
        unique-id type derivation).

        Use this endpoint while designing a direct integration, before sending events to
        `POST /v3/erp/updates/events`. See documentation at /docs/DIRECT_MODE.md.
      tags:
      - erp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DirectSimulationRequest'
            examples:
              entity_upsert:
                summary: Entity upsert (open mode)
                value:
                  event_configuration:
                    direct: true
                  payload:
                    version: '1'
                    operations:
                    - entity_slug: contact
                      unique_ids:
                        customer_number: CUST-12345
                      attributes:
                        first_name: Anna
                        last_name: Schmidt
                        email: anna.schmidt@example.com
              allowlist_with_relation:
                summary: Allowlisted entities with a relation
                value:
                  event_configuration:
                    direct: true
                    entities:
                    - entity_schema: contract
                      unique_ids:
                      - contract_number
                    - entity_schema: contact
                      unique_ids:
                      - customer_number
                  payload:
                    version: '1'
                    operations:
                    - entity_slug: contract
                      unique_ids:
                        contract_number: CTR-001
                      attributes:
                        display_name: Premium Contract
                        customer:
                          $relation:
                            _set:
                            - schema: contact
                              unique_ids:
                                customer_number: CUST-12345
                              tags:
                              - primary
              meter_reading:
                summary: Meter reading
                value:
                  event_configuration:
                    direct: true
                  payload:
                    version: '1'
                    operations:
                    - type: meter_reading
                      meter:
                        unique_ids:
                          meter_number: M-001
                      counter:
                        unique_ids:
                          obis_number: 1-0:1.8.0
                      reading_matching: strict-date
                      attributes:
                        external_id: R-001
                        timestamp: '2026-08-24T06:00:00Z'
                        source: ERP
                        value: 12345.6
      responses:
        '200':
          description: Simulation executed - `valid` reports the verdict, `errors` lists every violation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DirectSimulationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Unprocessable entity - simulation could not be executed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/erp/updates/mapping_simulation:
    post:
      operationId: simulateMapping
      summary: simulateMapping
      description: |
        Test mapping configuration by transforming a payload using the provided mapping rules without persisting data.

        Supports both v1.0 (object-based) and v2.0 (event-based) mapping formats.
        See documentation at /docs/MAPPING_V2.md for detailed v2.0 format specification.
      tags:
      - erp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MappingSimulationRequest'
            examples:
              v1_example:
                summary: Version 1.0 Example
                value:
                  mapping_configuration:
                    version: '1.0'
                    mapping:
                      objects:
                        business_partner:
                          unique_ids:
                            contact:
                              partner_id: _id
                          fields:
                          - entity: contact
                            attribute: first_name
                            field: firstName
                          - entity: contact
                            attribute: last_name
                            field: lastName
                  object_type: business_partner
                  format: json
                  payload: '{"partner_id":"BP10001","firstName":"John","lastName":"Doe"}'
              v2_simple:
                summary: Version 2.0 - Simple Example
                value:
                  mapping_configuration:
                    version: '2.0'
                    mapping:
                      events:
                        CustomerChanged:
                          entities:
                          - entity_schema: contact
                            unique_ids:
                            - customer_number
                            fields:
                            - attribute: customer_number
                              field: customerId
                            - attribute: first_name
                              field: firstName
                            - attribute: last_name
                              field: lastName
                            - attribute: email
                              field: email
                            - attribute: source
                              constant: ERP
                  object_type: CustomerChanged
                  format: json
                  payload: '{"customerId":"12345","firstName":"Anna","lastName":"Schmidt","email":"anna@example.com"}'
              v2_advanced:
                summary: Version 2.0 - Advanced with Relations
                value:
                  mapping_configuration:
                    version: '2.0'
                    mapping:
                      events:
                        ContractChanged:
                          entities:
                          - entity_schema: contract
                            unique_ids:
                            - contract_number
                            fields:
                            - attribute: contract_number
                              field: number
                            - attribute: display_name
                              field: displayName
                            - attribute: billing_account
                              relations:
                                operation: _set
                                items:
                                - entity_schema: billing_account
                                  _tags:
                                  - PRIMARY
                                  unique_ids:
                                  - attribute: external_id
                                    field: customerAccountId
                          - entity_schema: meter
                            unique_ids:
                            - meter_number
                            jsonataExpression: 'meters.{"number": number, "type": meterType}'
                            fields:
                            - attribute: meter_number
                              field: number
                            - attribute: meter_type
                              field: type
                  object_type: ContractChanged
                  format: json
                  payload: '{"number":"CTR-001","displayName":"Premium Contract","customerAccountId":"ACC-999","meters":[{"number":"M-001","meterType":"electricity"},{"number":"M-002","meterType":"gas"}]}'
              v2_append_operation:
                summary: Version 2.0 - Using _append Operation
                description: Example showing how to use _append to add relations to existing entities
                value:
                  mapping_configuration:
                    version: '2.0'
                    mapping:
                      events:
                        ContactAdded:
                          entities:
                          - entity_schema: contract
                            unique_ids:
                            - contract_number
                            fields:
                            - attribute: contract_number
                              field: contractNumber
                            - attribute: additional_contacts
                              relations:
                                operation: _append
                                items:
                                - entity_schema: contact
                                  _tags:
                                  - ADDITIONAL
                                  unique_ids:
                                  - attribute: email
                                    field: contactEmail
                  object_type: ContactAdded
                  format: json
                  payload: '{"contractNumber":"CTR-001","contactEmail":"new.contact@example.com"}'
              v2_field_enabled:
                summary: Version 2.0 - Field-Level Conditional Processing
                description: Example showing how to conditionally enable/disable individual field mappings using the enabled property
                value:
                  mapping_configuration:
                    version: '2.0'
                    mapping:
                      events:
                        CustomerChanged:
                          entities:
                          - entity_schema: contact
                            unique_ids:
                            - customer_number
                            fields:
                            - attribute: customer_number
                              field: customerId
                            - attribute: email
                              field: email
                              enabled: true
                            - attribute: phone
                              field: phone
                              enabled: $exists(phone) and phone != ''
                            - attribute: mobile
                              field: mobile
                              enabled: $exists(mobile)
                            - attribute: vip_status
                              constant: VIP
                              enabled: vipFlag = true
                            - attribute: internal_notes
                              field: notes
                              enabled: false
                            - attribute: billing_account
                              enabled: $exists(billingAccountId)
                              relations:
                                operation: _set
                                items:
                                - entity_schema: billing_account
                                  unique_ids:
                                  - attribute: external_id
                                    field: billingAccountId
                  object_type: CustomerChanged
                  format: json
                  payload: '{"customerId":"12345","email":"user@example.com","phone":"","mobile":"555-1234","vipFlag":true,"notes":"Internal only","billingAccountId":"ACC-999"}'
      responses:
        '200':
          description: Successfully simulated mapping
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MappingSimulationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Unprocessable entity - mapping configuration or payload validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations:
    get:
      operationId: listIntegrations
      summary: listIntegrations
      description: Retrieve all integrations for the authenticated organization
      tags:
      - integrations
      responses:
        '200':
          description: Successfully retrieved integrations
          content:
            application/json:
              schema:
                type: object
                required:
                - integrations
                properties:
                  integrations:
                    type: array
                    items:
                      $ref: '#/components/schemas/Integration'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      operationId: createIntegration
      summary: createIntegration
      description: Create a new integration configuration
      tags:
      - integrations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateIntegrationRequest'
      responses:
        '201':
          description: Integration created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Integration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}:
    get:
      operationId: getIntegration
      summary: getIntegration
      description: Retrieve a specific integration by its ID
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved integration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Integration'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      operationId: updateIntegration
      summary: updateIntegration
      description: Update an existing integration configuration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateIntegrationRequest'
      responses:
        '200':
          description: Integration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Integration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteIntegration
      summary: deleteIntegration
      description: Delete an integration and all its use cases
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Integration deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/events:
    post:
      operationId: queryEvents
      summary: queryEvents
      description: Query events for a specific integration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryEventsRequest'
      responses:
        '200':
          $ref: '#/components/responses/QueryEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/events/replay:
    post:
      operationId: replayEvents
      summary: replayEvents
      description: Replay one or more events for a specific integration. Events will be re-processed with their original payloads but with a new correlation ID for traceability.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplayEventsRequest'
      responses:
        '200':
          $ref: '#/components/responses/ReplayEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/use-cases:
    get:
      operationId: listUseCases
      summary: listUseCases
      description: Retrieve all use cases for a specific integration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved use cases
          content:
            application/json:
              schema:
                type: object
                required:
                - use_cases
                properties:
                  use_cases:
                    type: array
                    items:
                      $ref: '#/components/schemas/UseCase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      operationId: createUseCase
      summary: createUseCase
      description: Create a new use case for an integration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUseCaseRequest'
      responses:
        '201':
          description: Use case created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UseCase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/use-cases/{useCaseId}:
    get:
      operationId: getUseCase
      summary: getUseCase
      description: Retrieve a specific use case by its ID
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved use case
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UseCase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Use case not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      operationId: updateUseCase
      summary: updateUseCase
      description: Update an existing use case configuration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUseCaseRequest'
      responses:
        '200':
          description: Use case updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UseCase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Use case not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteUseCase
      summary: deleteUseCase
      description: Delete a use case from an integration
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Use case deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Use case not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/use-cases/{useCaseId}/history:
    get:
      operationId: listUseCaseHistory
      summary: listUseCaseHistory
      description: |
        Retrieve historical versions of a use case's configuration.
        History entries are returned in reverse chronological order (newest first).
        Use the 'cursor' parameter for pagination to fetch additional entries.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      - name: cursor
        in: query
        required: false
        description: |
          Opaque pagination cursor. Pass the 'next_cursor' value from a previous
          response to fetch the next page of results.
        schema:
          type: string
        example: eyJwayI6Ik9SRyMxMjM0NSIsInNrIjoiSU5URUdSQVRJT04jLi4uIn0=
      responses:
        '200':
          description: Successfully retrieved use case history
          content:
            application/json:
              schema:
                type: object
                required:
                - history
                properties:
                  history:
                    type: array
                    description: History entries in reverse chronological order (newest first)
                    items:
                      $ref: '#/components/schemas/UseCaseHistoryEntry'
                  next_cursor:
                    type: string
                    description: Opaque cursor for fetching the next page. Absent if no more pages.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Use case not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/documentation:
    get:
      operationId: listDocumentationPages
      summary: listDocumentationPages
      description: |
        Retrieve all documentation pages of an integration, without their markdown content.
        An integration has at most one general page plus at most one page per use case.
        The page id is 'general' for the integration-wide page, otherwise the use case ID.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved documentation pages
          content:
            application/json:
              schema:
                type: object
                required:
                - pages
                properties:
                  pages:
                    type: array
                    items:
                      $ref: '#/components/schemas/DocumentationPageSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/documentation/{docId}:
    get:
      operationId: getDocumentationPage
      summary: getDocumentationPage
      description: Retrieve a single documentation page including its markdown content
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: docId
        in: path
        required: true
        description: '''general'' for the integration-wide page, otherwise a use case ID'
        schema:
          type: string
          pattern: ^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
      responses:
        '200':
          description: Successfully retrieved documentation page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentationPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Documentation page not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      operationId: upsertDocumentationPage
      summary: upsertDocumentationPage
      description: |
        Create or update the documentation page identified by docId.
        Upsert semantics enforce the invariant of one general page per
        integration and one page per use case. For use case pages the
        use case must exist.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: docId
        in: path
        required: true
        description: '''general'' for the integration-wide page, otherwise a use case ID'
        schema:
          type: string
          pattern: ^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertDocumentationPageRequest'
      responses:
        '200':
          description: Documentation page created or updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentationPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Integration or use case not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteDocumentationPage
      summary: deleteDocumentationPage
      description: Delete a documentation page
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: docId
        in: path
        required: true
        description: '''general'' for the integration-wide page, otherwise a use case ID'
        schema:
          type: string
          pattern: ^(general|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
      responses:
        '200':
          description: Documentation page deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Documentation page not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations:
    get:
      operationId: listIntegrationsV2
      summary: listIntegrationsV2
      description: Retrieve all integrations with embedded use cases for the authenticated organization
      tags:
      - integrations
      responses:
        '200':
          description: Successfully retrieved integrations with use cases
          content:
            application/json:
              schema:
                type: object
                required:
                - integrations
                properties:
                  integrations:
                    type: array
                    items:
                      $ref: '#/components/schemas/IntegrationWithUseCases'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      operationId: createIntegrationV2
      summary: createIntegrationV2
      description: |
        Create a new integration with embedded use cases.
      tags:
      - integrations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertIntegrationWithUseCasesRequest'
      responses:
        '201':
          description: Integration created successfully with use cases
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationWithUseCases'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}:
    get:
      operationId: getIntegrationV2
      summary: getIntegrationV2
      description: Retrieve a specific integration with all its embedded use cases
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved integration with use cases
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationWithUseCases'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      operationId: updateIntegrationV2
      summary: updateIntegrationV2
      description: |
        Update an existing integration with embedded use cases.
        The integration must already exist.
        Use cases are updated declaratively:
        - Use cases in the request with matching IDs are updated
        - Use cases in the request without matching IDs are created
        - Existing use cases not in the request are deleted
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID (client-provided)
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertIntegrationWithUseCasesRequest'
      responses:
        '200':
          description: Integration updated successfully with use cases
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationWithUseCases'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteIntegrationV2
      summary: deleteIntegrationV2
      description: Delete an integration and all its use cases
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Integration deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/notifications/history:
    get:
      operationId: listNotificationHistory
      summary: listNotificationHistory
      description: |
        Returns the cursor-paginated, newest-first notification history for an
        integration (every real notification decision — both fired and suppressed).
        Requires the `integration:view` permission on the integration's organization.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: cursor
        in: query
        required: false
        description: Opaque base64 pagination cursor returned as `next_cursor` by a prior page.
        schema:
          type: string
      - name: limit
        in: query
        required: false
        description: Requested number of items to return. Values above 100 are accepted and clamped server-side to 100 (the enforcement point is the handler, not this schema), so a large value never 400s at the contract layer.
        schema:
          type: integer
          minimum: 1
          default: 50
      - name: type
        in: query
        required: false
        description: Optional notification type filter (e.g. `critical_error`, `error_threshold`).
        schema:
          type: string
      responses:
        '200':
          description: Cursor-paginated notification history (newest first)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationHistoryResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/notifications/test:
    post:
      operationId: testSendNotification
      summary: testSendNotification
      description: |
        Renders and sends ONE representative notification of the requested kind/type to
        the CALLING USER ONLY (never any other recipient), so an operator can preview how
        a notification looks. A test send does NOT write to the notification history.
        Requires the `integration:manage` permission on the integration's organization.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestNotificationRequest'
      responses:
        '202':
          description: Test notification accepted for delivery to the calling user (sent is true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestNotificationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '502':
          description: |
            The downstream notification service rejected or failed the test send.
            The body uses TestNotificationResponse with sent set to false so the
            operator sees the real (failed) delivery outcome.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestNotificationResponse'
  /v2/integrations/{integrationId}/notifications/status:
    get:
      operationId: getNotificationStatus
      summary: getNotificationStatus
      description: |
        Returns the live per-rule alert state and (for 'auto' rules) the current
        hour-of-week baseline band for an integration's notification monitoring.
        Reflects the latest 5-minute sweep — near-real-time, not live.
        Requires the `integration:view` permission on the integration's organization.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: include
        in: query
        required: false
        description: |
          Add `baseline_series` to also return all 168 hour-of-week buckets per
          'auto' rule (heavier; omit for just the current-bucket markers).
        schema:
          type: string
          enum:
          - baseline_series
      responses:
        '200':
          description: Live notification status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationStatusResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist:
    get:
      operationId: getSecureProxyWhitelist
      summary: getSecureProxyWhitelist
      description: |
        Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case.
        Staff-only — gated by internal-auth issuer AND admin-portal Cognito user pool membership.
        Rejects Login-As tokens.
      tags:
      - integrations
      - proxy
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Current whitelist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecureProxyWhitelist'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Use case not found or not of type secure_proxy
    put:
      operationId: updateSecureProxyWhitelist
      summary: updateSecureProxyWhitelist
      description: |
        Replaces allowed_domains and/or allowed_ips on a secure_proxy use case.
        At least one of the two fields is required. Validation mirrors the CLI's
        `validateDomainPatterns` / `validateCidrs`. Writes a USECASE_HISTORY row
        with the admin user's email as `changed_by`.

        Update semantics per field:
          - **omitted** — field is not modified; the stored value is preserved.
          - **non-empty array** — the stored value is replaced with the supplied list.
          - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is
            the canonical way to remove all entries. `null` is not accepted.

        Staff-only — same auth gates as GET.
      tags:
      - integrations
      - proxy
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SecureProxyWhitelistUpdate'
      responses:
        '200':
          description: Updated whitelist (same shape as GET)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecureProxyWhitelist'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Use case not found or not of type secure_proxy
  /v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist/history:
    get:
      operationId: listSecureProxyWhitelistHistory
      summary: listSecureProxyWhitelistHistory
      description: |
        Returns the most recent USECASE_HISTORY entries for a secure_proxy use case,
        in reverse chronological order (newest first). Each entry includes the
        actor email (`changed_by`), the ISO-8601 timestamp (`history_created_at`),
        the `change_description` (free-text action), and the full `configuration`
        snapshot — from which UI-08 computes a before/after diff between consecutive
        entries.

        Staff-only — gated by internal-auth issuer AND admin-portal Cognito user pool
        membership. Rejects Login-As tokens (same auth gate as the GET / PUT
        secure-proxy-whitelist operations).

        Thin wrapper over the service-layer `listUseCaseHistory` that powers
        `GET /v1/integrations/{integrationId}/use-cases/{useCaseId}/history` — the
        separate path exists because the /v1 variant is tenant-gated and admin-portal
        internal-auth tokens do not carry tenant permissions.
      tags:
      - integrations
      - proxy
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: useCaseId
        in: path
        required: true
        description: The use case ID (must be of type secure_proxy)
        schema:
          type: string
          format: uuid
      - name: limit
        in: query
        required: false
        description: |
          Maximum number of history entries to return. Default 10, max 50.
          Capped at the service-layer page size (20) so `limit > 20` is silently
          clamped to 20. UI-08 requests 5-10 for the panel view.
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 10
      responses:
        '200':
          description: Successfully retrieved secure_proxy history
          content:
            application/json:
              schema:
                type: object
                required:
                - history
                properties:
                  history:
                    type: array
                    description: |
                      Entries are `SecureProxyUseCaseHistoryEntry` in reverse
                      chronological order (newest first). Entries older than the
                      requested `limit` are not returned.
                    items:
                      $ref: '#/components/schemas/SecureProxyUseCaseHistoryEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Use case not found or not of type secure_proxy
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/app-mapping:
    put:
      operationId: setIntegrationAppMapping
      summary: setIntegrationAppMapping
      description: |
        Creates or updates a mapping from an app/component to an integration.
        This allows ERP updates sent via app_id and component_id to be associated
        with a specific integration configuration.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID to map to
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetIntegrationAppMappingRequest'
      responses:
        '200':
          description: App mapping set successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationAppMapping'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Integration not found
        '409':
          description: Mapping already exists (use overwrite=true to replace)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteIntegrationAppMapping
      summary: deleteIntegrationAppMapping
      description: |
        Removes a mapping from an app/component to an integration.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID (used for authorization, must match the mapping)
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteIntegrationAppMappingRequest'
      responses:
        '200':
          description: App mapping deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: App mapping not found
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/monitoring/inbound-events:
    post:
      operationId: queryInboundMonitoringEvents
      summary: queryInboundMonitoringEvents
      description: |
        Query inbound monitoring events for a specific integration.
        Returns detailed information about inbound sync events from ERP systems,
        including success rates, error breakdowns, and processing metrics.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryInboundMonitoringEventsRequest'
      responses:
        '200':
          $ref: '#/components/responses/QueryInboundMonitoringEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/monitoring/stats:
    post:
      operationId: getMonitoringStats
      summary: getMonitoringStats
      description: |
        Get aggregated statistics for both inbound and outbound monitoring events for a specific integration.
        Returns summary metrics for inbound (ERP sync) and outbound (webhook delivery) events,
        including success/error counts and optional breakdowns.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetMonitoringStatsRequest'
      responses:
        '200':
          $ref: '#/components/responses/GetMonitoringStatsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/monitoring/timeseries:
    post:
      operationId: getMonitoringTimeSeries
      summary: getMonitoringTimeSeries
      description: |
        Get time-series aggregated event counts for monitoring charts.
        Returns pre-bucketed counts at configurable intervals for both inbound and outbound events.
        Maximum of 200 buckets per request. Returns 400 if the time range and interval would exceed this limit.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetMonitoringTimeSeriesRequest'
      responses:
        '200':
          $ref: '#/components/responses/GetMonitoringTimeSeriesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound-status:
    get:
      operationId: getOutboundStatus
      summary: getOutboundStatus
      description: |
        Get the status of all outbound use cases for a specific integration.
        Returns conflict information when events or webhooks are disabled but the use case is enabled.
      tags:
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved outbound status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/entities/{entityId}/sync-status:
    get:
      operationId: getEntitySyncStatus
      summary: getEntitySyncStatus
      description: |
        Get the inbound ERP sync status of an entity: when each integration last
        synchronized (checked) the entity against the ERP, and when it last
        actually changed it. `last_synced_at` also advances on no-op checks —
        events that were received and evaluated but changed nothing — which by
        design leave no trace on the entity itself (no activity feed entry, no
        `_updated_at` bump). Use it to tell whether an entity is up to date with
        the ERP. Returns an empty list for entities no inbound use case has
        processed. Org-scoped via the caller's token.
      tags:
      - integrations
      parameters:
      - name: entityId
        in: path
        required: true
        description: The entity ID
        schema:
          type: string
          format: uuid
      - name: integration_id
        in: query
        required: false
        description: Narrow the result to a single integration
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Successfully retrieved entity sync status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntitySyncStatusResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound/messages/poll:
    post:
      operationId: pollOutboundMessages
      summary: pollOutboundMessages
      description: |
        Poll outbound messages for an integration's poll-mode use cases.
        Takes a lease on the head-of-line batch of the integration's FIFO stream:
        the returned messages stay invisible to subsequent polls until they are
        acknowledged or the visibility timeout elapses. POST because taking a
        lease mutates server state — auto-retrying middleware must not burn
        leases. One in-flight batch per stream: while a lease is active (or
        another poll wins the race), the response is an empty batch
        (`messages: []`), not an error. Requires the `integration:consume` grant.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PollOutboundMessagesRequest'
      responses:
        '200':
          description: Leased batch of outbound messages (possibly empty)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PollOutboundMessagesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound/messages/ack:
    post:
      operationId: ackOutboundMessages
      summary: ackOutboundMessages
      description: |
        Acknowledge polled outbound messages. Acks are validated against the
        active lease and committed as a prefix-contiguous cursor advance:
        messages must be acknowledged in stream order. Out-of-order acks past
        the first gap are rejected per id (`out_of_order`), acks with an
        outdated lease token are rejected (`stale_lease`), and unknown ids are
        rejected (`not_found`). Acknowledged messages are never delivered
        again. Requires the `integration:consume` grant.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AckOutboundMessagesRequest'
      responses:
        '200':
          description: Per-id acknowledgement results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AckOutboundMessagesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound/messages/dlq:
    get:
      operationId: listOutboundDlqMessages
      summary: listOutboundDlqMessages
      description: |
        List an integration's dead-lettered outbound queue messages
        (poison_policy enforcement and operator skips move messages here).
        Operator endpoint — requires the `integration:manage` grant. Message
        payloads are NOT included in listings: entries are keyed by the
        opaque message id and carry delivery metadata only. Paginated via an
        opaque `next_token`.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: limit
        in: query
        required: false
        description: Maximum number of DLQ entries to return
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
      - name: next_token
        in: query
        required: false
        description: Opaque pagination token from a previous response
        schema:
          type: string
      responses:
        '200':
          description: Page of dead-lettered messages
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundDlqListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound/messages/dlq/redrive:
    post:
      operationId: redriveOutboundDlqMessages
      summary: redriveOutboundDlqMessages
      description: |
        Redrive selected dead-lettered messages back into the live stream.
        Operator endpoint — requires the `integration:manage` grant.
        A redriven message is re-enqueued at the tail with a new id and
        sequence — it is delivered out of its original per-entity order (the
        stream has moved on); this is inherent to redrive and matches SQS DLQ
        semantics. The redriven copy starts with zero delivery attempts and a
        fresh retention window; the original DLQ entry is removed. Per-id
        results report `redriven` or `not_found` (unknown ids, or entries
        concurrently redriven/expired).
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedriveOutboundDlqRequest'
      responses:
        '200':
          description: Per-id redrive results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedriveOutboundDlqResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/outbound/messages/unblock:
    post:
      operationId: unblockOutboundStream
      summary: unblockOutboundStream
      description: |
        Unblock an integration's outbound stream halted by the `block`
        poison policy: skips (dead-letters) the current blocked head message,
        emitting MSG_DEAD_LETTERED and letting the next message become the
        head. Operator endpoint — requires the `integration:manage` grant.
        Returns `unblocked: false` as a no-op when the stream is not
        currently blocked (or the state moved concurrently) — safe to retry.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnblockOutboundStreamRequest'
      responses:
        '200':
          description: Unblock outcome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnblockOutboundStreamResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/monitoring/access-logs:
    post:
      operationId: queryAccessLogs
      summary: queryAccessLogs
      description: |
        Query API access logs for a specific integration's organization.
        Returns access token usage analytics filtered by user_id (access token).
        Supports infinite scroll pagination with cursor-based navigation.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID (used for tenant authorization)
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryAccessLogsRequest'
      responses:
        '200':
          $ref: '#/components/responses/QueryAccessLogsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/monitoring/outbound-events:
    post:
      operationId: queryOutboundMonitoringEvents
      summary: queryOutboundMonitoringEvents
      description: |
        Query outbound monitoring events for a specific integration.
        Returns detailed information about outbound event deliveries,
        filtered by event_name (event_catalog_event) linked to the integration's outbound use cases.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryOutboundMonitoringEventsRequest'
      responses:
        '200':
          $ref: '#/components/responses/QueryOutboundMonitoringEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/events:
    post:
      operationId: queryMonitoringEventsV2
      summary: queryMonitoringEventsV2
      description: |
        Query monitoring events from the unified erp_monitoring_v2 table.
        Returns all event types (inbound, outbound, file_proxy, etc.) in a single list.
        Replaces the separate v1 inbound-events and outbound-events endpoints.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryMonitoringEventsV2Request'
      responses:
        '200':
          $ref: '#/components/responses/QueryMonitoringEventsV2Response'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/stats:
    post:
      operationId: getMonitoringStatsV2
      summary: getMonitoringStatsV2
      description: |
        Get aggregated statistics from the unified erp_monitoring_v2 table.
        Returns combined metrics for all event types with optional breakdowns.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetMonitoringStatsV2Request'
      responses:
        '200':
          $ref: '#/components/responses/GetMonitoringStatsV2Response'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/time-series:
    post:
      operationId: getMonitoringTimeSeriesV2
      summary: getMonitoringTimeSeriesV2
      description: |
        Get time-series aggregated event counts from the unified erp_monitoring_v2 table.
        Returns bucketed counts for chart rendering.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetMonitoringTimeSeriesV2Request'
      responses:
        '200':
          $ref: '#/components/responses/GetMonitoringTimeSeriesV2Response'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/events/{eventId}/associated:
    get:
      operationId: getAssociatedMonitoringEvents
      summary: getAssociatedMonitoringEvents
      description: |
        Returns all monitoring events sharing the same event_id, ordered chronologically.
        Also includes the original inbound event payload from erp_incoming_events if available.
        Used to display a full event trace/timeline.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: eventId
        in: path
        required: true
        description: The event ID to get associated events for
        schema:
          type: string
      responses:
        '200':
          $ref: '#/components/responses/GetAssociatedMonitoringEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/external-events:
    post:
      operationId: ingestExternalMonitoringEvents
      summary: ingestExternalMonitoringEvents
      description: |
        Ingest monitoring spans produced by an EXTERNAL system (e.g. an integration
        middleware), so the Integration Hub is the central monitoring point and the
        cross-system event trace spans both the external system and epilot's own
        processing.

        Each span is validated, assigned a server-controlled `EXTERNAL_*` taxonomy code
        derived from its `level` (the client never supplies a code — this marks
        provenance and prevents spoofing an epilot code), has its `use_case_slug`
        resolved against the integration's configured use cases, and is republished onto
        the same monitoring event bus as epilot's own spans — so external spans are
        first-class to the trace view, stats, alerting and digests.

        Spans link to the epilot trace via `correlation_id` (the trace id): the middleware
        must stamp the same `correlation_id` here and on the event it forwards to the
        inbound endpoint. Invalid spans in a batch are reported per-item and do not fail
        the whole batch.
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestExternalMonitoringEventsRequest'
      responses:
        '202':
          description: Batch accepted (some spans may have been rejected — see counts).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestExternalMonitoringEventsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/integrations/{integrationId}/monitoring/traces/{correlationId}:
    get:
      operationId: getMonitoringTraceByCorrelation
      summary: getMonitoringTraceByCorrelation
      description: |
        Returns the cross-system event trace for a `correlation_id`: every monitoring
        span sharing it — external spans (middleware) plus epilot's own processing spans
        — ordered chronologically, with a rolled-up status and the epilot inbound event
        "head" attached. This is the correlation-grouped counterpart to
        `…/events/{eventId}/associated` (which groups a single epilot event's fan-out by
        event_id). Origin is distinguished by the `EXTERNAL_*` code prefix on external
        spans. Freshness is near-real-time (spans land as they are produced/ingested).
      tags:
      - monitoring
      - integrations
      parameters:
      - name: integrationId
        in: path
        required: true
        description: The integration ID
        schema:
          type: string
          format: uuid
      - name: correlationId
        in: path
        required: true
        description: The trace id (correlation_id) shared across systems
        schema:
          type: string
      responses:
        '200':
          description: The assembled cross-system trace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MonitoringTraceResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/secure-proxies:
    get:
      operationId: listSecureProxies
      summary: listSecureProxies
      description: |
        Lists all secure_proxy use cases across all integrations for the authenticated organization.
        Returns minimal data suitable for dropdowns and selection UIs.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      responses:
        '200':
          description: Successfully retrieved secure proxy use cases
          content:
            application/json:
              schema:
                type: object
                required:
                - secure_proxies
                properties:
                  secure_proxies:
                    type: array
                    items:
                      $ref: '#/components/schemas/SecureProxySummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/secure-proxy:
    post:
      operationId: secureProxy
      summary: secureProxy
      description: |
        Routes an HTTP request through a VPC with either static IP egress or VPN secure link access.
        The VPC mode is determined by the referenced secure_proxy use case configuration.
        For secure_link mode, the target URL must match the use case's allowed_domains whitelist.
      tags:
      - proxy
      security:
      - EpilotAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SecureProxyRequest'
      responses:
        '200':
          description: Proxied response from the target URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecureProxyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden - insufficient permissions or target domain not whitelisted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '502':
          description: |
            Upstream failure: epilot could not obtain an HTTP response from the proxied target
            (e.g. TLS handshake failure, connection refused/reset, DNS failure or timeout).
            The body's `code`/`reason` identify the underlying cause, distinguishing a
            remote-side problem from an epilot-side one. When the target *does* answer with
            its own status, that status and body are passed through unchanged via
            `SecureProxyResponse` instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecureProxyUpstreamError'
        '503':
          description: VPC proxy not available for the requested mode
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '504':
          description: Upstream timeout
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
  /v1/managed-call/{slug}/execute:
    post:
      operationId: managedCallExecute
      summary: managedCallExecute
      description: |
        Execute a managed call operation synchronously. The slug in the path acts as the RPC method name.
        Calls an external partner API with JSONata mapping on both request and response.
      tags:
      - managed-call
      security:
      - EpilotAuth: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
        description: Use case slug (acts as the RPC method name)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ManagedCallExecuteRequest'
      responses:
        '200':
          description: |
            Managed call executed successfully.
            The response body is the JSONata-mapped result (or raw external API response if no mapping).
            If inbound routing is configured, check X-Inbound-Event-Id header for tracking.
          headers:
            X-Inbound-Event-Id:
              description: Event ID for tracking inbound pipeline processing (only present when inbound routing is configured)
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedCallExecuteResponse'
        '400':
          description: Bad request (e.g., invalid URL blocked by SSRF protection)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedCallErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '502':
          description: External API error (upstream service returned an error)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedCallErrorResponse'
  /v1/integrations/{integrationId}/generate-types-preview:
    post:
      operationId: generateTypesPreview
      summary: generateTypesPreview
      description: |
        Analyses the JSONata mappings of all managed-call use cases in the integration and returns scaffolded type descriptors. The frontend uses these to show the type editor modal where developers fill in leaf types.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Scaffolded type preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateTypesPreviewResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/generate-types:
    post:
      operationId: generateTypes
      summary: generateTypes
      description: |
        Generates a complete TypeScript npm package with typed interfaces for all managed-call use cases. This is a stateless operation that does not persist any changes. Use the commit-types endpoint to lock configurations after review.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateTypesRequest'
      responses:
        '200':
          description: Generated package
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateTypesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/integrations/{integrationId}/commit-types:
    post:
      operationId: commitTypes
      summary: commitTypes
      description: |
        Commits the generated types by locking use case configurations and updating version tracking. Should be called after the user reviews and downloads the generated package.
      tags:
      - integrations
      security:
      - EpilotAuth: []
      parameters:
      - name: integrationId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitTypesRequest'
      responses:
        '200':
          description: Types committed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitTypesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports:
    post:
      operationId: createErpImport
      summary: createErpImport
      description: >-
        Register an already-uploaded file (S3 ref) as a pricing-file import job. Returns the job and a file preview. Nothing runs yet: no use case is chosen and no validation starts here. Optionally rank candidates with POST
        /v2/erp/imports/{importId}:suggest-use-cases, then start validation with POST /v2/erp/imports/{importId}:validate.

        Pass `import_id` to repoint an existing PENDING import at a different file instead, keeping its id and its place in the history.
      tags:
      - erp-imports
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateErpImportRequest'
      responses:
        '200':
          description: The named import now points at this file. Returned only when `import_id` was supplied — no `Location`, because nothing was created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateErpImportResponse'
        '201':
          description: Import job created. Returned only when `import_id` was omitted.
          headers:
            Location:
              description: URL of the created job (`/v2/erp/imports/{importId}`).
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateErpImportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
    get:
      operationId: listErpImports
      summary: listErpImports
      description: |
        List recent pricing-file import jobs for the org, newest first.

        Pass `next_cursor` back as `cursor` for the next page. A page can be
        shorter than `limit` and still have more behind it, so stop on
        `next_cursor: null`.
      tags:
      - erp-imports
      parameters:
      - name: limit
        in: query
        required: false
        description: Page size. Values above 100 are clamped to 100.
        schema:
          type: integer
          minimum: 1
          default: 50
      - name: cursor
        in: query
        required: false
        description: Opaque cursor from a prior page's `next_cursor`.
        schema:
          type: string
      responses:
        '200':
          description: Import jobs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpImportList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports/{importId}:
    get:
      operationId: getErpImport
      summary: getErpImport
      description: Get a pricing-file import job (status, counts, result links).
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The import job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpImportJob'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteErpImport
      summary: deleteErpImport
      description: 'Remove an import and the file it owns. Allowed from any status: an import whose run is still in flight is stopped by the deletion, and rows it already wrote stay written.'
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: The import and its file were removed
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports/{importId}:validate:
    post:
      operationId: validateErpImport
      summary: validateErpImport
      description: |-
        Choose the use case to read this file with, and start the validate phase.
        Callable from PENDING, READY and FAILED — so a wrong choice is corrected by calling this again with a different use case, rather than re-uploading. Each call replaces any previous verdict. Any other status returns 409.
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateErpImportRequest'
      responses:
        '202':
          description: Validation started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpImportJob'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: The use case cannot drive an import
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports/{importId}:suggest-use-cases:
    post:
      operationId: suggestErpImportUseCases
      summary: suggestErpImportUseCases
      description: >-
        Rank the org's inbound use cases against this file's columns — the input to the ranked picker ("matches 6 of your 7 columns"). Optional: skip this and call `:validate` directly when the use case is already known. Reads only the
        file's first row, not its data.

        Every eligible use case is returned, including ones matching nothing: the "nothing fits, pick anyway" view needs the full list. Highest match first.

        CSV only — an xlsx import fails with 400 rather than silently returning zero matches, which would look identical to "we checked and nothing matches".
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Ranked use-case suggestions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuggestErpImportUseCasesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports/{importId}:execute:
    post:
      operationId: executeErpImport
      summary: executeErpImport
      description: Confirm and run the write phase of a validated import. Only a READY job may be executed; any other status returns 409.
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecuteErpImportRequest'
      responses:
        '202':
          description: Execution started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpImportJob'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: The verdict carries warnings and `ack_warnings` was not true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseBase'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v2/erp/imports/{importId}:abort:
    post:
      operationId: abortErpImport
      summary: abortErpImport
      description: >-
        Ask a running import to stop. Valid while the job is VALIDATING or PROCESSING; any other status returns 409.

        The stop is cooperative: the job passes through CANCELLING and typically reaches CANCELLED within seconds.

        Rows already published to the platform are **kept**: this is a stop, not a rollback. Aborting during VALIDATING has published nothing, so it is always clean; aborting during PROCESSING leaves a partial import, and the rows that
        landed are visible in monitoring under the job's correlation_id.
      tags:
      - erp-imports
      parameters:
      - name: importId
        in: path
        required: true
        schema:
          type: string
      responses:
        '202':
          description: The stop was recorded. The job is still finishing up — poll GET /v2/erp/imports/{importId} to observe CANCELLED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpImportJob'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  securitySchemes:
    EpilotAuth:
      type: http
      scheme: bearer
      description: Authorization header with epilot OAuth2 bearer token
      bearerFormat: JWT
  schemas:
    S3Reference:
      type: object
      required:
      - bucket
      - key
      properties:
        bucket:
          type: string
        key:
          type: string
    CreateErpImportRequest:
      type: object
      description: Register an already-uploaded file as an import. The use case is chosen later, via `:validate` — upload and interpretation are separate decisions.
      required:
      - s3_reference
      additionalProperties: false
      properties:
        s3_reference:
          $ref: '#/components/schemas/S3Reference'
        include_preview:
          type: boolean
          default: false
          description: Return a sample of the file's first rows in `preview`. Only controls whether the sample comes BACK — the head is read either way, because that read is how an unreadable file gets refused.
        import_id:
          type: string
          description: >-
            Repoint an EXISTING import at this file instead of registering a new one — same row, same id, so iterating on which file to import leaves one entry in the history rather than one per attempt.

            Only a PENDING import may be repointed; any other status returns 409 with code `IMPORT_NOT_REPLACEABLE`. Past the first check the previous verdict is what tells the user what to fix, and a job that reached the execute phase may
            already have published rows under `correlation_id`.

            An id that names nothing returns 404. It never falls back to registering a new import: a stale id must fail loudly rather than quietly produce a second one.
    ValidateErpImportRequest:
      type: object
      required:
      - integration_id
      - use_case_slug
      additionalProperties: false
      properties:
        integration_id:
          type: string
        use_case_slug:
          type: string
          description: An inbound use case's slug, unique within its integration.
    ErpImportUseCaseSuggestion:
      type: object
      required:
      - integration_id
      - integration_name
      - use_case_slug
      - use_case_name
      - matched_columns
      - entity_types
      properties:
        integration_id:
          type: string
        integration_name:
          type: string
        use_case_slug:
          type: string
        use_case_name:
          type: string
        entity_types:
          type: integer
          description: How many distinct entity schemas this mapping uses.
        matched_columns:
          type: integer
          description: How many of the FILE's columns this use case reads, by exact trimmed name.
    SuggestErpImportUseCasesResponse:
      type: object
      required:
      - file_columns
      - suggestions
      properties:
        file_columns:
          type: integer
          description: Columns in the file. Present even when `suggestions` is empty.
        suggestions:
          type: array
          items:
            $ref: '#/components/schemas/ErpImportUseCaseSuggestion'
    ExecuteErpImportRequest:
      type: object
      description: Confirmation options. Required only when the verdict carries warnings — a clean import needs no body at all.
      additionalProperties: false
      properties:
        ack_warnings:
          type: boolean
          description: The caller has seen `validation.issues` and accepts them. Required when `validation.warnings` is greater than 0; ignored otherwise.
    ErpImportIssue:
      type: object
      description: |-
        A problem found during validation, scoped to the file as a whole rather than to individual rows.
        `code` is the translation key and the other fields are its parameters — there is deliberately no message to display. Each code appears at most once, with everything it has to say aggregated into that one entry.
      required:
      - code
      - severity
      properties:
        code:
          type: string
          description: |
            Enum of possible issue codes.
          enum:
          - UNIQUE_ID_COLUMN_MISSING
          - MAPPED_COLUMN_MISSING
          - MALFORMED_ROW
          - INVALID_ENCODING
          - EMPTY_FILE
          - TOO_MANY_ROWS
          - BLANK_ROWS_SKIPPED
        severity:
          type: string
          enum:
          - warning
          - blocking
        columns:
          type: array
          description: >-
            The columns this issue is about, at most one entry per column per entity.

            On UNIQUE_ID_COLUMN_MISSING the file has NONE of these. Do NOT tell the user that adding one of them is enough: a unique id may read several columns through a JSONata expression, and whether it combines them (`A & B`, both
            needed) or falls back between them (`A ? A : B`, either will do) is not knowable here.
          items:
            type: object
            required:
            - name
            properties:
              name:
                type: string
                description: The column name, spelled as the mapping reads it.
              entity:
                type: string
                description: Slug of the entity this column helps identify. UNIQUE_ID_COLUMN_MISSING only.
        row:
          type: integer
          description: The offending data row, 1-based as the user counts rows. MALFORMED_ROW only.
    ErpImportValidation:
      type: object
      description: 'Validate-phase summary: what the file will create, and whether it may be confirmed. Absent until the validate phase completes. No per-row detail is kept — a rejected file is corrected and imported again.'
      required:
      - total_rows
      - blocking
      - warnings
      - entities
      properties:
        total_rows:
          type: integer
          description: Data rows the import will act on. Rows with no value in any column are dropped before they are counted, and reported as BLANK_ROWS_SKIPPED — so this can be lower than the line count of the file.
        blocking:
          type: integer
          description: Blocking problems found, counting per-row ones that are not listed in `issues`.
        warnings:
          type: integer
        entities:
          type: object
          additionalProperties:
            type: integer
          description: Distinct entities the file expresses, keyed by entity slug.
        issues:
          type: array
          description: >-
            Whole-file issues, at most one per `code`. Do not expect the length to match blocking + warnings: those also count per-row problems, which are recorded for support but never listed here. Warnings here are what `ack_warnings` on
            `:execute` acknowledges.
          items:
            $ref: '#/components/schemas/ErpImportIssue'
    ErpImportProgress:
      type: object
      description: >-
        How far the currently running phase has got. Written at every batch boundary, so it advances during long runs rather than only at the end.

        `total_rows` is ABSENT during the validate phase until the file has been read to the end — there is deliberately no counting pass, since that would be a second unbounded read of the whole file. Render an indeterminate indicator
        while it is missing: dividing by a missing total yields a determinate bar pinned at 0%, which reads as a hung import.
      required:
      - processed_rows
      properties:
        processed_rows:
          type: integer
          description: Rows fully processed. An exact "the first N rows are done" watermark, not an estimate — it only advances once a batch has been completely handled.
        total_rows:
          type: integer
          description: Rows in the file. Known only once a phase has read to EOF; the execute phase has it from the start, because validate recorded it first.
    ErpImportError:
      type: object
      description: Why the import failed — present if and only if status = FAILED. `code` is the translation key; for VALIDATION_BLOCKED the specifics are in `validation`.
      required:
      - code
      - message
      properties:
        code:
          type: string
          description: |
            Enum of possible error codes.
          enum:
          - VALIDATION_BLOCKED
          - FILE_FORMAT_UNSUPPORTED
          - FILE_UNAVAILABLE
          - VALIDATE_TIMEOUT
          - IMPORT_TIMEOUT
          - USE_CASE_NOT_USABLE
          - IMPORT_NO_PROGRESS
          - INTERNAL_ERROR
        message:
          type: string
          description: >-
            One English sentence, derived from `code` so the two always agree. A fallback for a client that has no translation for this code — prefer translating `code`, and never parse this. It deliberately does NOT restate
            `validation.issues`.
    ErpImportFilePreview:
      type: object
      description: Sample of the file's first rows, using the same parser as `:validate`. Registration refuses a file it cannot read, so a created job always includes this.
      required:
      - columns
      - rows
      properties:
        columns:
          type: array
          description: Effective column names (trimmed, duplicates collapsed).
          items:
            type: string
        rows:
          type: array
          description: Up to the first 3 data rows, aligned to `columns`.
          items:
            type: array
            items:
              type: string
    CreateErpImportResponse:
      type: object
      required:
      - job
      properties:
        job:
          $ref: '#/components/schemas/ErpImportJob'
        preview:
          $ref: '#/components/schemas/ErpImportFilePreview'
    ErpImportJob:
      type: object
      required:
      - import_id
      - org_id
      - format
      - status
      - s3_input_ref
      - created_at
      - updated_at
      properties:
        import_id:
          type: string
          description: '`imp_{ULID}` — time-ordered, also used as the job''s correlation_id.'
        org_id:
          type: string
        created_by:
          type: string
        integration_id:
          type: string
          description: The integration owning the chosen use case. Absent until the first `:validate`.
        use_case_slug:
          type: string
          description: The inbound use case whose mapping drives both phases. Absent until the first `:validate`, and MUTABLE — re-validating with a different use case replaces it.
        format:
          type: string
          enum:
          - csv
          - xlsx
        status:
          type: string
          description: >-
            PENDING → VALIDATING → READY → PROCESSING → IMPORTED, with FAILED reachable from any working status, and CANCELLING → CANCELLED reachable from VALIDATING or PROCESSING via :abort. Only IMPORTED and CANCELLED are terminal. READY
            and FAILED both accept a further `:validate`, which is how a wrong use case is corrected without re-uploading the file.

            IMPORTED means every row was handed to the platform, not that the platform finished — per-row outcomes live in monitoring, filtered by correlation_id. A file that fails validation is FAILED with error.code = VALIDATION_BLOCKED.

            READY is legitimately idle for as long as the user takes to confirm, so it carries no running work and never goes stale.

            CANCELLING is transient and cooperative: the abort has been recorded but the worker only notices at its next batch boundary. Rows already published stay published — a stop is not a rollback.
          enum:
          - PENDING
          - VALIDATING
          - READY
          - PROCESSING
          - IMPORTED
          - FAILED
          - CANCELLING
          - CANCELLED
        s3_input_ref:
          $ref: '#/components/schemas/S3Reference'
        size_bytes:
          type: integer
          format: int64
          description: Size of the uploaded file, recorded at registration. Present on every job registered from version 1.17.0 onwards; absent on older rows, which were written before it was captured.
        column_count:
          type: integer
          description: >-
            How many effective columns the file's header yielded — the length of the `preview.columns` returned at registration, kept so a job loaded later can still describe its file. The preview ROWS are deliberately not stored: they are
            a sample for the person about to choose a mapping, not job state. Present on every job registered from version 1.17.0 onwards.
        validation:
          $ref: '#/components/schemas/ErpImportValidation'
        progress:
          $ref: '#/components/schemas/ErpImportProgress'
        error:
          $ref: '#/components/schemas/ErpImportError'
        correlation_id:
          type: string
          description: Scopes this run in monitoring. Always equal to `import_id`.
        activity_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ErpImportList:
      type: object
      required:
      - results
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ErpImportJob'
        next_cursor:
          type: string
          nullable: true
          description: Cursor for the next page, or null when there are no more rows.
    NotificationHistoryItem:
      type: object
      description: A single notification-history row (one real notification decision).
      required:
      - id
      - type
      - severity
      - title
      - occurred_at
      - notified
      - recipients
      - context
      - created_at
      properties:
        id:
          type: string
          description: Stable history row id (ULID).
        type:
          type: string
          description: The notification/rule type (e.g. critical_error, error_threshold, integration_digest).
        state_transition:
          type: string
          nullable: true
          description: The state transition that produced this decision (e.g. OK->ALERTING), when applicable.
        severity:
          type: string
          enum:
          - error
          - warning
          - info
          description: Severity of the decision.
        title:
          type: string
          description: Human-readable title at decision time.
        occurred_at:
          type: string
          format: date-time
          description: When the decision occurred (newest-first ordering key).
        notified:
          type: boolean
          description: Whether the notification was actually sent (true) or suppressed (false).
        suppressed_reason:
          type: string
          nullable: true
          enum:
          - muted
          - debounced
          - recipient_opt_out
          description: Why the notification was suppressed (only set when notified is false).
        recipients:
          type: array
          description: epilot user ids the notification was (or would have been) delivered to.
          items:
            type: string
        context:
          type: object
          additionalProperties: true
          description: Type-specific context captured at decision time.
        created_at:
          type: string
          format: date-time
          description: When the history row was written.
    NotificationHistoryResponse:
      type: object
      required:
      - history
      properties:
        history:
          type: array
          description: Notification-history rows, newest first.
          items:
            $ref: '#/components/schemas/NotificationHistoryItem'
        next_cursor:
          type: string
          nullable: true
          description: Opaque base64 cursor for the next page, or null when there are no more rows.
    TestNotificationRequest:
      type: object
      required:
      - kind
      properties:
        kind:
          type: string
          enum:
          - alert
          - digest
          description: The kind of notification to render and send.
        type:
          type: string
          description: The alert type to render when kind=alert (e.g. critical_error, error_threshold).
        channels:
          type: array
          description: Delivery channels to use; defaults to the integration's configured channels.
          items:
            type: string
            enum:
            - email
            - in_app
    TestNotificationResponse:
      type: object
      required:
      - sent
      - recipient
      - channels
      properties:
        sent:
          type: boolean
          description: Whether the test notification was accepted for delivery.
        recipient:
          type: string
          description: The calling user id the test was sent to (the only recipient).
        channels:
          type: array
          description: The channels the test was delivered on.
          items:
            type: string
        notification_id:
          type: string
          nullable: true
          description: The svc-notification-api message id, or null when not returned.
    NotificationStatusResponse:
      type: object
      required:
      - health
      - rules
      properties:
        health:
          type: string
          enum:
          - healthy
          - alerting
          - muted
          description: |
            Rolled-up live status: `muted` when muteUntil is in the future; else `alerting` if any rule is currently ALERTING; else `healthy`.
        evaluated_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Most recent per-rule evaluation instant (max lastEvaluatedAt), or null when no rule has been evaluated. Updates on the 5-minute sweep tick.
        rules:
          type: array
          description: Per-rule status, one entry per configured rule.
          items:
            $ref: '#/components/schemas/NotificationRuleStatus'
    NotificationRuleStatus:
      type: object
      required:
      - rule_id
      - state
      properties:
        rule_id:
          type: string
          description: The rule's stable id (matches the configured rule id).
        state:
          type: string
          enum:
          - ok
          - alerting
          - recovered
          description: The rule's live AlertState (defaults to `ok` when never evaluated).
        last_fired_at:
          type: string
          format: date-time
          nullable: true
          description: ISO instant the rule last entered ALERTING.
        last_cleared_at:
          type: string
          format: date-time
          nullable: true
          description: ISO instant the rule last cleared back to OK.
        baseline:
          nullable: true
          description: Present only for enabled 'auto'-threshold rules; null otherwise.
          allOf:
          - $ref: '#/components/schemas/RuleBaselineStatus'
    RuleBaselineStatus:
      type: object
      required:
      - is_mature
      properties:
        is_mature:
          type: boolean
          description: |
            False during cold start; the sweeper uses the static fallbackThreshold until the baseline's history span is mature.
        computed_at:
          type: string
          format: date-time
          nullable: true
          description: ISO instant the baseline was last computed.
        median:
          type: number
          nullable: true
          description: Typical in-scope event volume for the current hour-of-week bucket.
        mad:
          type: number
          nullable: true
          description: Median absolute deviation for the current hour-of-week bucket.
        upper:
          type: number
          nullable: true
          description: |
            Dynamic alert threshold (median + k·MAD, k by sensitivity) for the current hour-of-week, or null when the bucket is uncovered.
        buckets:
          type: array
          nullable: true
          description: Full 168-bucket series; only present when ?include=baseline_series.
          items:
            $ref: '#/components/schemas/RuleBaselineBucket'
    RuleBaselineBucket:
      type: object
      required:
      - dow
      - hour
      - median
      - mad
      properties:
        dow:
          type: integer
          minimum: 1
          maximum: 7
          description: Day of week, 1=Monday … 7=Sunday.
        hour:
          type: integer
          minimum: 0
          maximum: 23
          description: Hour of day, 0 … 23 (UTC).
        median:
          type: number
        mad:
          type: number
    ErrorResponseBase:
      type: object
      properties:
        code:
          type: string
          description: Computer-readable error code
        message:
          type: string
          description: Error message
    ErpEvent:
      type: object
      required:
      - event_type
      - object_type
      - timestamp
      - format
      - payload
      properties:
        event_type:
          type: string
          enum:
          - CREATE
          - UPDATE
          - DELETE
          description: Type of event (create, update, delete)
        object_type:
          type: string
          description: Type of the object being updated (business_partner, contract_account, etc.). Corresponds to "Event Name" from the integration UI.
        timestamp:
          type: string
          format: date-time
          description: Timestamp when the event occurred
        format:
          type: string
          enum:
          - json
          - xml
          default: json
          description: Format of the payload data
        payload:
          oneOf:
          - type: string
            minLength: 2
            description: The serialized object data payload (JSON, XML, etc.) as a string
          - type: object
            minProperties: 1
            description: Direct JSON object (will be automatically serialized)
            additionalProperties: true
          description: The object data payload - can be either a serialized string or a direct JSON object
        deduplication_id:
          type: string
          pattern: ^[a-zA-Z0-9_-]+$
          minLength: 1
          maxLength: 255
          description: |
            Optional unique identifier for idempotency - prevents duplicate processing of the same event within 24 hours in context of the same integration. Must contain only alphanumeric characters, hyphens, and underscores.
          example: evt-2025-05-01-12345-create-bp
        correlation_id:
          type: string
          description: >
            Optional per-event trace id for cross-system tracing (unique per business operation). Overrides the request-level meta.correlation_id for THIS event. When absent, the event inherits the request-level correlation_id; when both
            are absent, epilot mints its own event_id and the trace is epilot-only. Orthogonal to deduplication_id (idempotency).
          example: bp-8f3a2c-7d4e-4b1a-9c2f-1e6d5a4b3c21
        use_case_id:
          type: string
          nullable: true
          readOnly: true
          description: |
            Resolved use case ID for the inbound event. Null when no use case matched or for events ingested before this field was introduced. Server-populated only — ignored if supplied on inbound requests.
    ErpUpdatesEventsV2Request:
      type: object
      required:
      - integration_id
      - events
      properties:
        integration_id:
          type: string
          format: uuid
          description: UUID that identifies the integration configuration to use
        correlation_id:
          type: string
          description: Optional ID that identifies the specific request for debugging purposes
        events:
          type: array
          description: List of ERP events to process
          items:
            $ref: '#/components/schemas/ErpEvent'
    ErpEventV3:
      type: object
      required:
      - timestamp
      - format
      - payload
      anyOf:
      - required:
        - event_name
      - required:
        - use_case_slug
      properties:
        event_name:
          type: string
          description: |
            Event name from integration mapping (e.g., business_partner, contract_account). Required when use_case_slug is not provided.
          example: business_partner
        timestamp:
          type: string
          format: date-time
          description: Timestamp when the event occurred
          example: '2025-05-01T08:30:00Z'
        format:
          type: string
          enum:
          - json
          - xml
          default: json
          description: Format of the payload data
          example: json
        payload:
          oneOf:
          - type: string
            minLength: 2
            description: The serialized object data payload (JSON, XML, etc.) as a string
          - type: object
            minProperties: 1
            description: Direct JSON object (will be automatically serialized)
            additionalProperties: true
          description: |
            The object data payload - can be either a serialized string or a direct JSON object.

            For events routed to a mapped (default) use case this is the raw ERP object; the use
            case's mapping configuration transforms it into entity updates.

            For events routed to a use case whose configuration has `direct: true`, the payload
            MUST be a `DirectPayload` (as an object, or a JSON string): a versioned envelope of
            pre-mapped entity and meter reading operations that skip the mapping engine entirely.
            Direct use cases accept JSON only - `format: "xml"` events are rejected.
          example: '{"id":"BP10001","name":"Acme Corporation","type":"organization","tax_id":"DE123456789","status":"active"}'
        use_case_slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          minLength: 1
          maxLength: 255
          description: >
            Recommended. Use case slug for routing this event to the correct use case configuration. If provided, takes precedence over event_name for use case lookup. Preferred over event_name-based routing as slugs are portable across
            environments.
          example: business_partner
        deduplication_id:
          type: string
          pattern: ^[a-zA-Z0-9_-]+$
          minLength: 1
          maxLength: 255
          description: |
            Optional unique identifier for idempotency - prevents duplicate processing of the same event within 24 hours in context of the same integration. Must contain only alphanumeric characters, hyphens, and underscores.
          example: evt-2025-05-01-12345-bp
        group_id:
          type: string
          description: >
            Optional per-event ordering group. Overrides the request-level `group_id` for THIS event; when absent, the event inherits the request-level `group_id` (or the default strict per-integration ordering). Use it when a single batch
            carries events for unrelated business objects that may be processed in parallel. See the request-level `group_id` for semantics.
          maxLength: 128
          example: customer-42
        correlation_id:
          type: string
          minLength: 1
          maxLength: 255
          description: >
            Optional per-event trace id. Overrides the request-level `correlation_id` for THIS event, so a batch carrying several distinct business operations keeps its traces segregated in monitoring instead of merged under one
            request-level id. When absent, the event inherits the request-level `correlation_id`.
          example: customer-42-sync-2026-08-26
    ErpUpdatesEventsV3Request:
      type: object
      required:
      - integration_id
      - events
      properties:
        integration_id:
          type: string
          format: uuid
          description: UUID that identifies the integration configuration to use
        correlation_id:
          type: string
          description: Optional ID that identifies the specific request for debugging purposes
        group_id:
          type: string
          description: |
            Controls ordering and parallelism for this request's events.

            By default, all events for a given `integration_id` are processed
            **strictly in order, one at a time**. For high-volume integrations this
            can become a throughput bottleneck.

            Set `group_id` to opt into **parallel processing**:
            - Events sharing the same `group_id` are processed in the order received.
            - Events with different `group_id` values are processed in parallel.

            Typical usage is to derive `group_id` from a logical partition key in
            your payload — for example the customer ID, contract ID, or meter ID —
            so updates to the same business object remain ordered while unrelated
            objects are processed concurrently.

            Notes:
            - Individual events may set their own `group_id`, which overrides this
              request-level value for that event only.
            - Up to 20 groups per integration are processed concurrently. Using
              more distinct values than that yields no additional parallelism.
            - Omit this field if strict per-integration ordering is required.
          maxLength: 128
          example: customer-42
        events:
          type: array
          description: List of ERP events to process
          items:
            $ref: '#/components/schemas/ErpEventV3'
    TriggerErpActionRequest:
      type: object
      required:
      - execution_id
      - org_id
      - webhook_id
      - flow_id
      - created_at
      - action_id
      - flow_action_id
      - flow_name
      - activity_id
      - entity_id
      properties:
        execution_id:
          type: string
          description: Unique identifier of the current automation execution
        org_id:
          type: string
          description: Identifier of the organization where the automation is executed
        webhook_id:
          type: string
          description: Identifier of the self-service webhook configuration
        flow_id:
          type: string
          description: Identifier of the automation flow that triggered the action
        created_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp when the webhook event was created
        action_id:
          type: string
          description: Identifier of the automation action being executed
        flow_action_id:
          type: string
          description: Identifier of the specific automation flow action instance
        flow_name:
          type: string
          description: Human readable name of the automation flow
        activity_id:
          type: string
          description: Identifier of the entity activity related to this trigger
        entity_id:
          type: string
          description: Identifier of the entity referenced by the activity
    TriggerWebhookResp:
      type: object
      properties:
        status_code:
          type: string
        message:
          type: string
        body:
          type: object
        code:
          type: string
        status:
          type: string
          enum:
          - succeeded
          - failed
        start_date:
          type: string
        end_date:
          type: string
        event_id:
          type: string
    IntegrationEditableFields:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Integration name
        description:
          type: string
          maxLength: 1000
          description: Optional description of the integration
        access_token_ids:
          type: array
          description: List of access token IDs associated with this integration
          items:
            type: string
        app_ids:
          type: array
          description: List of app IDs associated with this integration
          items:
            type: string
        environment_config:
          type: array
          items:
            $ref: '#/components/schemas/EnvironmentFieldConfig'
          description: Configuration defining environment variables needed by this integration. Values are stored in the Environments API.
        settings:
          $ref: '#/components/schemas/IntegrationSettings'
        integration_type:
          type: string
          enum:
          - erp
          - connector
          default: erp
          description: |
            Type of integration. "erp" is the ERP integration with inbound/outbound use cases. "connector" is for complex proxy integrations with external APIs.
        connector_config:
          $ref: '#/components/schemas/ConnectorConfig'
        protected:
          type: boolean
          description: If true, integration is displayed in read-only mode in the UI to discourage changes
        _manifest:
          type: array
          description: The manifest IDs associated with this integration
          items:
            type: string
    ConnectorConfig:
      type: object
      description: Shared configuration for connector-type integrations
      properties:
        base_url:
          type: string
          description: Base URL for the partner API
        auth:
          $ref: '#/components/schemas/ManagedCallAuth'
        types_versions:
          type: array
          description: History of generated type package versions
          items:
            type: object
            required:
            - version
            - package_name
            - generated_at
            - generated_by
            - status
            properties:
              version:
                type: string
              package_name:
                type: string
              generated_at:
                type: string
                format: date-time
              generated_by:
                type: string
              status:
                type: string
                enum:
                - active
                - deprecated
        latest_types_version:
          type: string
          description: Latest active types package version
        latest_types_package_name:
          type: string
          description: Latest active types package name
    ManagedCallAuth:
      type: object
      description: Authentication configuration for managed call requests
      properties:
        type:
          type: string
          enum:
          - oauth2_client_credentials
          - api_key
          - bearer
          description: Authentication type
        token_url:
          type: string
          description: OAuth2 token URL. Can be plain text or {{env.key}} reference.
        client_id:
          type: string
          description: OAuth2 client ID. Can be plain text or {{env.key}} reference.
        client_secret:
          type: string
          description: OAuth2 client secret. Must be an {{env.key}} reference (secret).
        scope:
          type: string
          description: OAuth2 scope
        audience:
          type: string
          description: OAuth2 audience parameter (e.g. for Auth0, Azure AD). Can be plain text or {{env.key}} reference.
        resource:
          type: string
          description: OAuth2 resource parameter (e.g. for Azure AD). Can be plain text or {{env.key}} reference.
        body_params:
          type: object
          additionalProperties:
            type: string
          description: Additional key-value pairs for the OAuth2 token request body. Values can be {{env.key}} references.
        headers:
          type: object
          additionalProperties:
            type: string
          description: Additional headers for the OAuth2 token request. Values can be {{env.key}} references.
        query_params:
          type: object
          additionalProperties:
            type: string
          description: Additional query parameters for the OAuth2 token URL. Values can be {{env.key}} references.
        api_key_header:
          type: string
          description: Header name for API key auth (default X-API-Key)
        api_key:
          type: string
          description: API key value. Must be an {{env.key}} reference (secret).
        token:
          type: string
          description: Bearer token value. Must be an {{env.key}} reference (secret).
    Integration:
      allOf:
      - type: object
        required:
        - id
        - orgId
        - name
        - created_at
        - updated_at
        properties:
          id:
            type: string
            format: uuid
            readOnly: true
            description: Unique identifier for the integration
          orgId:
            type: string
            readOnly: true
            description: Organization ID
          created_at:
            type: string
            format: date-time
            readOnly: true
            description: ISO-8601 timestamp when the integration was created
          updated_at:
            type: string
            format: date-time
            readOnly: true
            description: ISO-8601 timestamp when the integration was last updated
      - $ref: '#/components/schemas/IntegrationEditableFields'
    CreateIntegrationRequest:
      allOf:
      - $ref: '#/components/schemas/IntegrationEditableFields'
      - type: object
        required:
        - name
    UpdateIntegrationRequest:
      $ref: '#/components/schemas/IntegrationEditableFields'
    EnvironmentFieldConfig:
      type: object
      required:
      - key
      - label
      - type
      properties:
        key:
          type: string
          description: Environment variable key, used to look up the value in the Environments API.
          pattern: ^[a-z0-9][a-z0-9_.\-]{0,127}$
        label:
          type: string
          description: Display label for the field in the UI
          minLength: 1
          maxLength: 255
        type:
          type: string
          enum:
          - String
          - SecretString
          description: Whether the value is a plain string or an encrypted secret
        description:
          type: string
          description: Help text shown below the field
          maxLength: 1000
        required:
          type: boolean
          default: false
          description: Whether this field must be filled before the integration can be used
        order:
          type: integer
          minimum: 0
          description: Sort order for display and drag-to-reorder
    IntegrationSettings:
      type: object
      description: Settings for the integration
      properties:
        autoRefresh:
          $ref: '#/components/schemas/AutoRefreshSettings'
        notifications:
          $ref: '#/components/schemas/IntegrationNotificationConfig'
    AutoRefreshSettings:
      type: object
      description: Auto-refresh settings for keeping integration data fresh
      properties:
        enabled:
          type: boolean
          default: false
          description: Whether auto-refresh is enabled
        freshnessThresholdMinutes:
          type: integer
          minimum: 1
          description: Maximum age (in minutes) of data before it is considered stale and eligible for refresh
    IntegrationNotificationConfig:
      type: object
      description: >-
        Integration monitoring notification configuration. Rides Integration.settings.notifications (camelCase) and surfaces on both v1 and v2 GET/PUT. Unknown keys are stripped server-side to stay forward-compatible with deferred (V2) rule
        types.
      required:
      - enabled
      - recipients
      - defaultChannels
      - rules
      - digest
      properties:
        enabled:
          type: boolean
          description: Master switch for this integration's notifications.
        recipients:
          type: array
          description: epilot user ids notified for this integration. Same-org membership and per-user notification preferences are enforced at send time (Phases 3–5), not at config-write time.
          items:
            $ref: '#/components/schemas/NotificationRecipient'
        defaultChannels:
          $ref: '#/components/schemas/NotificationChannelSet'
        monitoredUseCases:
          type: array
          description: Integration-level use-case include-filter; absent/empty means all use cases.
          items:
            type: string
        monitoredCodes:
          type: array
          description: Integration-level code scope; absent/empty resolves to ['_error_']. Accepts concrete monitoring error codes or group sentinels (_error_, _warning_, _success_, _info_, _any_, _parent_).
          items:
            type: string
        rules:
          type: array
          description: Enabled triggers and their params. A type MAY repeat; capped at 20 rules (enforced at the write boundary).
          items:
            $ref: '#/components/schemas/NotificationRule'
        digest:
          $ref: '#/components/schemas/NotificationDigestConfig'
        muteUntil:
          type: string
          format: date-time
          nullable: true
          description: ISO instant; snooze all non-digest alerts until this time. `null` means not muted.
    NotificationRecipient:
      type: object
      description: A configured recipient. Only the epilot user_id is stored.
      required:
      - user_id
      properties:
        user_id:
          type: string
          description: epilot user id. Same-org membership is enforced at send time (Phases 3–5), which re-validates each recipient against the integration's org before fanning out — it is not enforced at config-write time.
    NotificationChannelSet:
      type: object
      description: Delivery channel toggles. New channels added in svc-notification-api inherit here.
      required:
      - email
      - in_app
      properties:
        email:
          type: boolean
        in_app:
          type: boolean
    NotificationRule:
      type: object
      description: >-
        A single notification rule. Only the params relevant to a given type are set. The id is optional on write: the server mints a ULID when omitted and preserves a supplied id verbatim (a stable id keeps a rule's AlertState across
        config saves).
      required:
      - type
      - enabled
      properties:
        id:
          type: string
          description: Stable AlertState + baseline key. Optional on write — the server mints a ULID when omitted; a supplied id is preserved verbatim.
        name:
          type: string
          description: Optional human label disambiguating two rules of the same type.
        type:
          type: string
          description: Rule trigger type. These are the only supported types; each is produced by a real alerter.
          enum:
          - critical_error
          - error_threshold
          - warning_threshold
          - success_rate_drop
          - recovery
          - silence
        enabled:
          type: boolean
        channels:
          $ref: '#/components/schemas/NotificationChannelSet'
        codes:
          type: array
          description: Per-rule code scope. Event-matching rules default to ['_parent_']; silence defaults to ['_any_']. success_rate_drop and recovery take no codes.
          items:
            type: string
        threshold:
          description: Count or percentage; 'auto' selects anomaly-baseline mode.
          oneOf:
          - type: number
          - type: string
            enum:
            - auto
        sensitivity:
          type: string
          description: Band width for 'auto' mode.
          enum:
          - low
          - medium
          - high
        fallbackThreshold:
          type: number
          description: Static value used while the 'auto' baseline is immature (cold start).
        window:
          type: string
          description: Evaluation window, e.g. '15m', '1h', '24h'.
        minSampleSize:
          type: integer
          description: success_rate_drop minimum sample size guard.
        quietPeriod:
          type: string
          description: silence quiet period, e.g. '12h'.
    NotificationDigestConfig:
      type: object
      description: Digest schedule and content configuration.
      required:
      - enabled
      - frequency
      - timeOfDay
      - timezone
      - channels
      - includeHealthy
      - skipIfEmpty
      properties:
        enabled:
          type: boolean
        frequency:
          type: string
          enum:
          - daily
          - weekly
        dayOfWeek:
          type: integer
          description: Weekly only. 0 = Sunday … 6 = Saturday.
          enum:
          - 0
          - 1
          - 2
          - 3
          - 4
          - 5
          - 6
        timeOfDay:
          type: string
          description: HH:mm
        timezone:
          type: string
          description: IANA timezone, e.g. 'Europe/Berlin'.
        channels:
          $ref: '#/components/schemas/NotificationChannelSet'
        includeHealthy:
          type: boolean
          description: List all integrations vs. only ones with issues.
        skipIfEmpty:
          type: boolean
          description: Suppress the digest when nothing happened.
    SetIntegrationAppMappingRequest:
      type: object
      required:
      - app_id
      - component_id
      properties:
        app_id:
          type: string
          format: uuid
          description: UUID of the integration app instance
        component_id:
          type: string
          format: uuid
          description: UUID of the integration app component instance
        overwrite:
          type: boolean
          default: false
          description: If true, overwrites existing mapping. If false and mapping exists, returns 409 Conflict.
    DeleteIntegrationAppMappingRequest:
      type: object
      required:
      - app_id
      - component_id
      properties:
        app_id:
          type: string
          format: uuid
          description: UUID of the integration app instance
        component_id:
          type: string
          format: uuid
          description: UUID of the integration app component instance
    IntegrationAppMapping:
      type: object
      required:
      - integration_id
      properties:
        integration_id:
          type: string
          format: uuid
          description: The integration ID this app/component is mapped to
    IntegrationWithUseCases:
      allOf:
      - $ref: '#/components/schemas/Integration'
      - type: object
        description: Integration with embedded use cases for atomic CRUD operations
        required:
        - use_cases
        properties:
          use_cases:
            type: array
            description: All use cases belonging to this integration
            items:
              $ref: '#/components/schemas/UseCase'
    UpsertIntegrationWithUseCasesRequest:
      allOf:
      - $ref: '#/components/schemas/IntegrationEditableFields'
      - type: object
        description: |
          Request to create or update an integration with embedded use cases (upsert).
          This is a declarative operation - the request represents the desired state.
        required:
        - name
        properties:
          use_cases:
            type: array
            description: |
              Full list of use cases (declarative). This replaces ALL existing use cases.
              - Use cases with an `id` field matching an existing use case will be updated
              - Use cases without an `id` or with a non-matching `id` will be created
              - Existing use cases not in this list will be deleted
            items:
              $ref: '#/components/schemas/EmbeddedUseCaseRequest'
    InboundIntegrationEventConfiguration:
      type: object
      description: Configuration for inbound use cases (ERP to epilot)
      properties:
        direct:
          type: boolean
          description: |
            Enables direct mode for this use case. When `true`, every event routed to this use
            case must carry a `DirectPayload` - pre-mapped entity and meter reading operations -
            instead of raw ERP data, and the mapping engine is skipped entirely. Everything else
            in the inbound pipeline (deduplication, ordering, create-vs-update resolution,
            relation resolution, monitoring) behaves exactly as in mapped mode.

            In direct mode, `entities` doubles as an optional allowlist: when non-empty, each
            operation's `entity_slug` must match an entry's `entity_schema`, and when that entry
            declares `unique_ids`, the operation's unique-id keys must be exactly that set (or
            just `_id`). Entries need only `entity_schema` and `unique_ids` - `fields` is not
            required and is ignored. `meter_readings` is ignored in direct mode.

            Flag changes take up to 5 minutes to propagate (configuration cache).
        entities:
          type: array
          description: Array of entity configurations for this event
          items:
            $ref: '#/components/schemas/IntegrationEntity'
        meter_readings:
          type: array
          description: Array of meter reading configurations for this event
          items:
            $ref: '#/components/schemas/IntegrationMeterReading'
    OutboundIntegrationEventConfiguration:
      type: object
      required:
      - event_catalog_event
      - mappings
      description: Configuration for outbound use cases. Defines the event that triggers the flow and the webhook mappings.
      properties:
        event_catalog_event:
          type: string
          description: The Event Catalog event name that triggers this outbound flow
          example: contract.created
        event_filter:
          type: string
          description: |
            JSONata boolean predicate over the hydrated event payload. The use case handles the
            event only when this evaluates truthy; when absent it handles every event of its name,
            which is the behaviour of every pre-existing configuration.

            This is where event scoping belongs — narrowing to certain ticket purposes, contract
            types or channels. Evaluation input is the full hydrated event, so relation nodes such
            as `ticket` and `contact` are populated. An expression that throws is treated as no
            match and logged, so one malformed filter cannot block the other use cases subscribed
            to the same event.
          example: $count(ticket._purpose[$ in ['bae4b4d1-d728-49ef-92ff-54486dd301b4']]) > 0
        mappings:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/OutboundMapping'
          description: List of mappings that transform and deliver the event
        ack_tracking:
          type: string
          enum:
          - 'on'
          - 'off'
          default: 'on'
          description: |
            Whether this use case participates in the acknowledgement protocol.

            `on` (the default, and the behaviour of every pre-existing use case) records an
            ACK_PENDING on each event and expects the consumer to confirm receipt via
            `POST /v1/erp/tracking/acknowledgement`; unconfirmed events raise ACK_TIMEOUT after
            the timeout window.

            `off` opts the use case out entirely: no tracking row, no ACK_PENDING, no
            ACK_TIMEOUT. Set it for consumers that never acknowledge — otherwise every event
            produces a guaranteed timeout warning — and for deliveries that already keep their
            own durable per-item record, such as `file_proxy`.

            The tracking row is per-event, not per-use-case, so it is suppressed only when EVERY
            enabled use case matching the event has opted out.
    IntegrationEntity:
      type: object
      required:
      - entity_schema
      - unique_ids
      properties:
        entity_schema:
          type: string
          description: Target entity schema (e.g., 'contact', 'contract')
        unique_ids:
          type: array
          items:
            type: string
          description: |
            Array of attribute names that uniquely identify this entity.
            The _type hint for repeatable fields (e.g., email, phone) should be specified
            on the corresponding field definition in the fields array.
        jsonataExpression:
          type: string
          description: Optional JSONata expression to pre-process the event data before field mapping
        enabled:
          oneOf:
          - type: boolean
          - type: string
          description: Controls whether this entity mapping should be processed. Can be a boolean or a JSONata expression (string) that evaluates to a boolean.
        mode:
          type: string
          enum:
          - upsert
          - delete
          - purge
          - upsert-prune-scope-purge
          - upsert-prune-scope-delete
          default: upsert
          description: |
            Operation mode for entity mapping:
            - 'upsert': Create or update the entity (default)
            - 'delete': Soft delete the entity (marks as deleted)
            - 'purge': Hard delete the entity (permanent removal)
            - 'upsert-prune-scope-purge': Upsert entities from array, then purge entities in scope that weren't upserted
            - 'upsert-prune-scope-delete': Upsert entities from array, then soft delete entities in scope that weren't upserted
        scope:
          $ref: '#/components/schemas/PruneScopeConfig'
        fields:
          type: array
          description: |
            Field mapping definitions. Required for mapped-mode entity configurations; may be
            omitted for allowlist entries of a `direct: true` use case, where no mapping runs.
          items:
            $ref: '#/components/schemas/IntegrationEntityField'
    IntegrationMeterReading:
      type: object
      required:
      - meter
      - fields
      properties:
        jsonataExpression:
          type: string
          description: |
            Optional JSONata expression to extract meter reading items from the event data.
            If not provided, the entire payload is used as the reading data.
            Useful when you need to extract an array of readings from a nested structure (e.g., "$.readings").
        reading_matching:
          type: string
          enum:
          - external_id
          - strict-date
          default: external_id
          description: |
            Strategy for matching incoming readings against existing readings.
            - 'external_id': Match readings by external_id attribute (default behavior)
            - 'strict-date': Match by meter_id + counter_id + direction + date (German timezone).
              Useful when readings originate from ECP and are echoed back by the ERP with truncated timestamps.
        mode:
          type: string
          enum:
          - upsert
          - delete
          - upsert-prune-scope
          default: upsert
          description: |
            Operation mode for meter reading mapping:
            - 'upsert': Create or update meter readings (default)
            - 'delete': Delete the meter reading
            - 'upsert-prune-scope': Upsert readings from array, then delete all other readings for the same meter+counter that weren't upserted
        scope:
          $ref: '#/components/schemas/MeterReadingPruneScopeConfig'
        meter:
          $ref: '#/components/schemas/MeterUniqueIdsConfig'
        meter_counter:
          $ref: '#/components/schemas/MeterUniqueIdsConfig'
        fields:
          type: array
          description: Field mapping definitions for meter reading attributes
          items:
            $ref: '#/components/schemas/IntegrationEntityField'
    PruneScopeConfig:
      type: object
      description: |
        Scope configuration for upsert-prune-scope modes.
        Defines how to find entities that should be pruned if not in the upsert payload.
        The scope is resolved against the original event payload (not individual array items).
      required:
      - scope_mode
      properties:
        scope_mode:
          type: string
          enum:
          - relations
          - query
          description: |
            Scope mode for finding entities to prune:
            - 'relations': Find scope by looking at all entities related to a specific entity (both direct and reverse relations)
            - 'query': Find scope entities directly via query parameters
        schema:
          type: string
          description: |
            For 'relations' mode: The schema of the entity to find (e.g., 'billing_account').
            Not used for 'query' mode.
        unique_ids:
          type: array
          description: |
            For 'relations' mode: How to identify the scope entity from the payload.
            Not used for 'query' mode.
          items:
            $ref: '#/components/schemas/RelationUniqueIdField'
        query:
          type: array
          description: |
            For 'query' mode: Direct query parameters to find scope entities.
            Not used for 'relations' or 'reverse-relations' modes.
          items:
            $ref: '#/components/schemas/RelationUniqueIdField'
    MeterReadingPruneScopeConfig:
      type: object
      description: |
        Scope configuration for meter reading upsert-prune-scope mode.
        The scope is all readings for the same meter + counter.
      properties:
        source:
          type: string
          description: |
            Optional source filter. When set, only readings with this source
            are eligible for pruning (e.g., 'ERP' to only prune ERP-synced readings).
    MeterUniqueIdsConfig:
      type: object
      required:
      - unique_ids
      properties:
        unique_ids:
          type: array
          minItems: 1
          description: Array of unique identifier field mappings
          items:
            $ref: '#/components/schemas/RelationUniqueIdField'
    IntegrationEntityField:
      type: object
      required:
      - attribute
      properties:
        attribute:
          type: string
          description: Target attribute name
        field:
          type: string
          description: Source field name or JSONPath expression (if starts with $)
        jsonataExpression:
          type: string
          description: JSONata expression for transformation
        constant:
          description: Constant value to assign (any type)
        _type:
          $ref: '#/components/schemas/RepeatableFieldType'
        enabled:
          oneOf:
          - type: boolean
          - type: string
          description: Controls whether this field mapping should be processed. Can be a boolean or a JSONata expression (string) that evaluates to a boolean. Defaults to true if omitted.
        relations:
          $ref: '#/components/schemas/RelationConfig'
        relation_refs:
          $ref: '#/components/schemas/RelationRefsConfig'
        file_proxy_url:
          $ref: '#/components/schemas/FileProxyUrlConfig'
        portal_ref:
          $ref: '#/components/schemas/PortalRefConfig'
        env_var_ref:
          $ref: '#/components/schemas/EnvVarRefConfig'
    FileProxyUrlParam:
      description: Parameter for file proxy URL. Exactly one of field, constant, or jsonataExpression must be set.
      oneOf:
      - type: object
        required:
        - field
        properties:
          field:
            type: string
            description: Source field name or JSONPath expression (if starts with $)
      - type: object
        required:
        - constant
        properties:
          constant:
            description: Constant value (any type, stringified for URL)
      - type: object
        required:
        - jsonataExpression
        properties:
          jsonataExpression:
            type: string
            description: JSONata expression for transformation
    FileProxyUrlParams:
      type: object
      description: Custom query parameters. Keys become URL param names, values resolved from payload.
      additionalProperties:
        $ref: '#/components/schemas/FileProxyUrlParam'
    FileProxyUrlConfig:
      description: |
        Auto-constructs a file proxy download URL. orgId and integrationId are injected from context. Exactly one of use_case_id or use_case_slug must be provided. Using use_case_slug is recommended as it is portable across environments.
      oneOf:
      - type: object
        required:
        - use_case_slug
        properties:
          use_case_slug:
            type: string
            pattern: ^[a-z0-9][a-z0-9_-]*$
            description: |
              Recommended. Slug of the file_proxy use case. Maps to useCaseSlug query parameter. Portable across environments.
          params:
            $ref: '#/components/schemas/FileProxyUrlParams'
      - type: object
        required:
        - use_case_id
        properties:
          use_case_id:
            type: string
            description: |
              Legacy. UUID of the file_proxy use case. Maps to useCaseId query parameter. Prefer use_case_slug for portable configuration.
          params:
            $ref: '#/components/schemas/FileProxyUrlParams'
    PortalOrigin:
      type: string
      enum:
      - END_CUSTOMER_PORTAL
      - INSTALLER_PORTAL
      - B2B_PORTAL
      - ADDITIONAL_PORTAL
      description: Origin/type of an epilot portal configuration.
    PortalRefFilter:
      type: object
      description: >
        Filter applied to the org's portal configurations before selection. All filters default to "match any" except `enabled` (default `true`) and `is_dummy` (default `false`). Set `enabled` or `is_dummy` to `null` to opt out of the
        default.
      properties:
        origin:
          oneOf:
          - $ref: '#/components/schemas/PortalOrigin'
          - type: array
            items:
              $ref: '#/components/schemas/PortalOrigin'
          description: Single origin or array of origins (matches if origin is in the array).
        enabled:
          type: boolean
          nullable: true
          description: Match portals with this `enabled` value. Default `true`. Set to `null` to ignore.
        is_dummy:
          type: boolean
          nullable: true
          description: Match portals with this `is_dummy` value. Default `false`. Set to `null` to ignore.
        is_epilot_domain:
          type: boolean
          description: Optional restriction on `is_epilot_domain`.
        name:
          type: string
          description: Exact match on the portal's `name`.
        domain:
          type: string
          description: Exact match on the portal's `domain`.
    PortalRefConfig:
      description: >
        Resolves to a property of one of the calling organization's epilot portal configurations at runtime, replacing hard-coded environment-specific portal UUIDs in inbound mappings. Matched portals are sorted ascending by `(_created_at,
        portal_id)`; portals without `_created_at` sort first (treated as oldest). When `select: "single"` matches more than one portal, the resolver still returns the oldest match and emits a `PORTAL_REF_AMBIGUOUS` warning.
      oneOf:
      - type: object
        properties:
          filter:
            $ref: '#/components/schemas/PortalRefFilter'
          select:
            type: string
            enum:
            - single
            - all
            default: single
            description: |
              `single` returns one literal value (the oldest matching portal); `all` returns an array of literal values (0, 1, or many).
          return:
            type: string
            enum:
            - portal_id
            - origin
            - domain
            - name
            default: portal_id
            description: Which portal field to emit.
      - type: object
        required:
        - return
        - jsonataExpression
        properties:
          filter:
            $ref: '#/components/schemas/PortalRefFilter'
          select:
            type: string
            enum:
            - single
            - all
            default: single
          return:
            type: string
            enum:
            - jsonata
            description: |
              Evaluate the sibling `jsonataExpression` against the matched portal(s). With `select: "single"` the input is the matched PortalConfig object; with `select: "all"` it is the full filtered+sorted array.
          jsonataExpression:
            type: string
            description: JSONata expression evaluated against the matched portal(s).
    EnvVarRefConfig:
      description: >
        Resolves to an org-scoped environment variable from the epilot environments-api service at runtime, replacing hard-coded environment-specific values (URLs, prefixes, identifiers) in inbound mappings. Secrets (`SecretString` values)
        are never exposed; the runtime treats both "missing key" and "secret-typed key" as identical `undefined` outcomes (no info disclosure via error code). For secret-resolving contexts (e.g. authorization headers in managed-call or
        file-proxy step configurations), use the templated `{{ env.<key> }}` syntax instead — that mechanism does decrypt secrets.
      oneOf:
      - type: object
        required:
        - key
        properties:
          key:
            type: string
            pattern: ^[a-z0-9][a-z0-9_.-]{0,127}$
            description: |
              Environment variable key. Must match the environments-api key contract (lowercase, digits, `_`, `.`, `-`; max 128 chars; starts with a lowercase letter or digit). Supports dot-namespaced keys like `erp_api.base_url`.
          default:
            type: string
            description: |
              Literal string returned when the key is missing (or is a secret). When provided, the `ENV_VAR_REF_NOT_FOUND` warning is suppressed because the author signalled the absence is expected.
          return:
            type: string
            enum:
            - value
            default: value
            description: |
              `value` returns the resolved env-var string as-is.
      - type: object
        required:
        - key
        - return
        - jsonataExpression
        properties:
          key:
            type: string
            pattern: ^[a-z0-9][a-z0-9_.-]{0,127}$
          default:
            type: string
          return:
            type: string
            enum:
            - jsonata
            description: |
              Evaluate the sibling `jsonataExpression` against the resolved env-var value (the input `$` is the string value).
          jsonataExpression:
            type: string
            description: JSONata expression evaluated against the resolved env-var value.
    EmbeddedUseCaseRequest:
      oneOf:
      - $ref: '#/components/schemas/EmbeddedInboundUseCaseRequest'
      - $ref: '#/components/schemas/EmbeddedOutboundUseCaseRequest'
      - $ref: '#/components/schemas/EmbeddedFileProxyUseCaseRequest'
      - $ref: '#/components/schemas/EmbeddedManagedCallUseCaseRequest'
      - $ref: '#/components/schemas/EmbeddedSecureProxyUseCaseRequest'
      discriminator:
        propertyName: type
        mapping:
          inbound: '#/components/schemas/EmbeddedInboundUseCaseRequest'
          outbound: '#/components/schemas/EmbeddedOutboundUseCaseRequest'
          file_proxy: '#/components/schemas/EmbeddedFileProxyUseCaseRequest'
          managed_call: '#/components/schemas/EmbeddedManagedCallUseCaseRequest'
          secure_proxy: '#/components/schemas/EmbeddedSecureProxyUseCaseRequest'
    EmbeddedUseCaseRequestBase:
      type: object
      required:
      - name
      - type
      - enabled
      properties:
        id:
          type: string
          format: uuid
          description: |
            Optional use case ID for update matching.
            - If provided and matches an existing use case, that use case is updated
            - If provided but no match, a new use case with this ID is created
            - If omitted, a new use case with auto-generated ID is created
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Use case name
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          minLength: 1
          maxLength: 255
          description: |
            URL-safe identifier for the use case. Optional on this upsert/sync endpoint — when omitted, the server derives one from the name and ensures uniqueness within the integration. Immutable after creation.
        enabled:
          type: boolean
          description: Whether the use case is enabled
        change_description:
          type: string
          maxLength: 2000
          description: Optional description of this change (like a commit message)
    EmbeddedInboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/EmbeddedUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - inbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    EmbeddedOutboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/EmbeddedUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - outbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/OutboundIntegrationEventConfiguration'
    EmbeddedFileProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/EmbeddedUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - file_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/FileProxyUseCaseConfiguration'
    EmbeddedManagedCallUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/EmbeddedUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - managed_call
            description: Use case type for managed API calls
          configuration:
            $ref: '#/components/schemas/ManagedCallOperationConfig'
    EmbeddedSecureProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/EmbeddedUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - secure_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/SecureProxyUseCaseConfiguration'
    UseCaseBase:
      type: object
      required:
      - id
      - integrationId
      - name
      - type
      - enabled
      - created_at
      - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the use case
        integrationId:
          type: string
          format: uuid
          description: Parent integration ID
        name:
          type: string
          description: Use case name
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          minLength: 1
          maxLength: 255
          description: |
            URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and underscores only.
        type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          description: Use case type
        enabled:
          type: boolean
        change_description:
          type: string
          maxLength: 2000
          description: |
            Reason given for the update that produced the current version. Absent when the update supplied none — it is not inherited from the previous version.
        changed_by:
          type: string
          description: |
            User ID recorded for the update that produced the current version. Absent when the update had no user context, e.g. an automated sync.
        created_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp when the use case was created
        updated_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp when the use case was last updated
    InboundUseCase:
      allOf:
      - $ref: '#/components/schemas/UseCaseBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - inbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    OutboundUseCase:
      allOf:
      - $ref: '#/components/schemas/UseCaseBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - outbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/OutboundIntegrationEventConfiguration'
    FileProxyUseCase:
      allOf:
      - $ref: '#/components/schemas/UseCaseBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - file_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/FileProxyUseCaseConfiguration'
    ManagedCallUseCase:
      allOf:
      - $ref: '#/components/schemas/UseCaseBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - managed_call
            description: Use case type for managed API calls
          configuration:
            $ref: '#/components/schemas/ManagedCallOperationConfig'
          type_annotations:
            $ref: '#/components/schemas/TypeAnnotations'
          types_locked:
            type: boolean
            description: Whether types have been generated for this use case
    SecureProxyUseCase:
      allOf:
      - $ref: '#/components/schemas/UseCaseBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - secure_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/SecureProxyUseCaseConfiguration'
    UseCase:
      oneOf:
      - $ref: '#/components/schemas/InboundUseCase'
      - $ref: '#/components/schemas/OutboundUseCase'
      - $ref: '#/components/schemas/FileProxyUseCase'
      - $ref: '#/components/schemas/ManagedCallUseCase'
      - $ref: '#/components/schemas/SecureProxyUseCase'
      discriminator:
        propertyName: type
        mapping:
          inbound: '#/components/schemas/InboundUseCase'
          outbound: '#/components/schemas/OutboundUseCase'
          file_proxy: '#/components/schemas/FileProxyUseCase'
          managed_call: '#/components/schemas/ManagedCallUseCase'
          secure_proxy: '#/components/schemas/SecureProxyUseCase'
    CreateUseCaseRequest:
      oneOf:
      - $ref: '#/components/schemas/CreateInboundUseCaseRequest'
      - $ref: '#/components/schemas/CreateOutboundUseCaseRequest'
      - $ref: '#/components/schemas/CreateFileProxyUseCaseRequest'
      - $ref: '#/components/schemas/CreateManagedCallUseCaseRequest'
      - $ref: '#/components/schemas/CreateSecureProxyUseCaseRequest'
      discriminator:
        propertyName: type
        mapping:
          inbound: '#/components/schemas/CreateInboundUseCaseRequest'
          outbound: '#/components/schemas/CreateOutboundUseCaseRequest'
          file_proxy: '#/components/schemas/CreateFileProxyUseCaseRequest'
          managed_call: '#/components/schemas/CreateManagedCallUseCaseRequest'
          secure_proxy: '#/components/schemas/CreateSecureProxyUseCaseRequest'
    CreateUseCaseRequestBase:
      type: object
      required:
      - name
      - slug
      - type
      - enabled
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Use case name
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          minLength: 1
          maxLength: 255
          description: >
            URL-safe identifier for the use case. Required for explicit creates so every use case has a portable cross-environment identifier. Must be unique per integration. Immutable after creation. Lowercase alphanumeric, hyphens, and
            underscores only.
        enabled:
          type: boolean
          description: Whether the use case is enabled
    CreateInboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/CreateUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - inbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    CreateOutboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/CreateUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - outbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/OutboundIntegrationEventConfiguration'
    CreateFileProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/CreateUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - file_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/FileProxyUseCaseConfiguration'
    CreateManagedCallUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/CreateUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - managed_call
            description: Use case type for managed API calls
          configuration:
            $ref: '#/components/schemas/ManagedCallOperationConfig'
    CreateSecureProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/CreateUseCaseRequestBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - secure_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/SecureProxyUseCaseConfiguration'
    UpdateUseCaseRequest:
      oneOf:
      - $ref: '#/components/schemas/UpdateInboundUseCaseRequest'
      - $ref: '#/components/schemas/UpdateOutboundUseCaseRequest'
      - $ref: '#/components/schemas/UpdateFileProxyUseCaseRequest'
      - $ref: '#/components/schemas/UpdateManagedCallUseCaseRequest'
      - $ref: '#/components/schemas/UpdateSecureProxyUseCaseRequest'
      discriminator:
        propertyName: type
        mapping:
          inbound: '#/components/schemas/UpdateInboundUseCaseRequest'
          outbound: '#/components/schemas/UpdateOutboundUseCaseRequest'
          file_proxy: '#/components/schemas/UpdateFileProxyUseCaseRequest'
          managed_call: '#/components/schemas/UpdateManagedCallUseCaseRequest'
          secure_proxy: '#/components/schemas/UpdateSecureProxyUseCaseRequest'
    UpdateUseCaseRequestBase:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Use case name
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          minLength: 1
          maxLength: 255
          description: |
            URL-safe identifier for the use case. Recommended for portable cross-environment referencing. Can only be set once on use cases that don't have a slug yet. Immutable after being set.
        enabled:
          type: boolean
          description: Whether the use case is enabled
        change_description:
          type: string
          maxLength: 2000
          description: Optional description of this change (like a commit message)
    UpdateInboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/UpdateUseCaseRequestBase'
      - type: object
        properties:
          type:
            type: string
            enum:
            - inbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    UpdateOutboundUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/UpdateUseCaseRequestBase'
      - type: object
        properties:
          type:
            type: string
            enum:
            - outbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/OutboundIntegrationEventConfiguration'
    UpdateFileProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/UpdateUseCaseRequestBase'
      - type: object
        properties:
          type:
            type: string
            enum:
            - file_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/FileProxyUseCaseConfiguration'
    UpdateManagedCallUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/UpdateUseCaseRequestBase'
      - type: object
        properties:
          type:
            type: string
            enum:
            - managed_call
            description: Use case type for managed API calls
          configuration:
            $ref: '#/components/schemas/ManagedCallOperationConfig'
          type_annotations:
            $ref: '#/components/schemas/TypeAnnotations'
    UpdateSecureProxyUseCaseRequest:
      allOf:
      - $ref: '#/components/schemas/UpdateUseCaseRequestBase'
      - type: object
        properties:
          type:
            type: string
            enum:
            - secure_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/SecureProxyUseCaseConfiguration'
    UseCaseHistoryEntry:
      oneOf:
      - $ref: '#/components/schemas/InboundUseCaseHistoryEntry'
      - $ref: '#/components/schemas/OutboundUseCaseHistoryEntry'
      - $ref: '#/components/schemas/FileProxyUseCaseHistoryEntry'
      - $ref: '#/components/schemas/ManagedCallUseCaseHistoryEntry'
      - $ref: '#/components/schemas/SecureProxyUseCaseHistoryEntry'
      discriminator:
        propertyName: type
        mapping:
          inbound: '#/components/schemas/InboundUseCaseHistoryEntry'
          outbound: '#/components/schemas/OutboundUseCaseHistoryEntry'
          file_proxy: '#/components/schemas/FileProxyUseCaseHistoryEntry'
          managed_call: '#/components/schemas/ManagedCallUseCaseHistoryEntry'
          secure_proxy: '#/components/schemas/SecureProxyUseCaseHistoryEntry'
    UseCaseHistoryEntryBase:
      type: object
      required:
      - id
      - useCaseId
      - integrationId
      - name
      - type
      - enabled
      - created_at
      - updated_at
      - history_created_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this history entry
        useCaseId:
          type: string
          format: uuid
          description: Reference to the parent use case
        integrationId:
          type: string
          format: uuid
          description: Parent integration ID
        name:
          type: string
          description: Use case name at this point in history
        slug:
          type: string
          description: Use case slug at this point in history
        enabled:
          type: boolean
          description: Whether the use case was enabled at this point in history
        change_description:
          type: string
          description: Description of the change that was made at this point in history
        changed_by:
          type: string
          description: User ID of the user who made the change that produced this history entry
        created_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp when the use case was originally created
        updated_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp of this historical snapshot (before the update)
        history_created_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp when this history entry was created
    InboundUseCaseHistoryEntry:
      allOf:
      - $ref: '#/components/schemas/UseCaseHistoryEntryBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - inbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    OutboundUseCaseHistoryEntry:
      allOf:
      - $ref: '#/components/schemas/UseCaseHistoryEntryBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - outbound
            description: Use case type
          configuration:
            $ref: '#/components/schemas/OutboundIntegrationEventConfiguration'
    FileProxyUseCaseHistoryEntry:
      allOf:
      - $ref: '#/components/schemas/UseCaseHistoryEntryBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - file_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/FileProxyUseCaseConfiguration'
    ManagedCallUseCaseHistoryEntry:
      allOf:
      - $ref: '#/components/schemas/UseCaseHistoryEntryBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - managed_call
            description: Use case type for managed API calls
          configuration:
            $ref: '#/components/schemas/ManagedCallOperationConfig'
          type_annotations:
            $ref: '#/components/schemas/TypeAnnotations'
          types_locked:
            type: boolean
            description: Whether types have been generated for this use case
    SecureProxyUseCaseHistoryEntry:
      allOf:
      - $ref: '#/components/schemas/UseCaseHistoryEntryBase'
      - type: object
        required:
        - type
        properties:
          type:
            type: string
            enum:
            - secure_proxy
            description: Use case type
          configuration:
            $ref: '#/components/schemas/SecureProxyUseCaseConfiguration'
    SecureProxyUseCaseConfiguration:
      type: object
      required:
      - vpc_mode
      description: |
        Configuration for secure_proxy use cases. Defines how to route requests through a secure VPC.
      properties:
        vpc_mode:
          type: string
          enum:
          - static_ip
          - secure_link
          readOnly: true
          description: |
            VPC routing mode. Read-only after creation.
            - static_ip: Routes through a VPC with static outbound IP (NAT Gateway) for IP-allowlisted external APIs.
            - secure_link: Routes through a VPN VPC for accessing private customer networks.
        allowed_domains:
          type: array
          items:
            type: string
          readOnly: true
          description: |
            Domain whitelist for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.
            Supports exact match (e.g., "api.wemag.com") and wildcard prefix (e.g., "*.wemag.com").
        allowed_ips:
          type: array
          items:
            type: string
          readOnly: true
          description: |
            IP allowlist (CIDR notation) for secure_link mode. Admin-only — can only be modified directly in DynamoDB via admin script.
            Required for secure_link mode. All DNS-resolved IPs must match at least one range.
            Example: ["10.0.1.0/24", "192.168.1.0/24"]
    SecureProxyWhitelist:
      type: object
      required:
      - vpc_mode
      - allowed_domains
      - allowed_ips
      description: |
        Current whitelist state for a secure_proxy use case. vpc_mode is read-only
        context so the UI can show the user what mode the pool is in.
      properties:
        vpc_mode:
          type: string
          nullable: true
          enum:
          - static_ip
          - secure_link
          description: VPC routing mode (read-only). `null` if not yet set.
        allowed_domains:
          type: array
          items:
            type: string
          description: |
            Exact domain ("api.example.com") or wildcard prefix ("*.example.com").
            Wildcards must have at least 2 labels in the suffix.
        allowed_ips:
          type: array
          items:
            type: string
          description: CIDR-notation IP ranges, e.g. "10.0.0.0/24".
    SecureProxyWhitelistUpdate:
      type: object
      description: |
        Partial update for a secure_proxy whitelist. At least one of
        `allowed_domains` or `allowed_ips` must be provided.

        Per-field semantics:
          - **omitted** — the field is not modified; the stored value is preserved.
          - **non-empty array** — the stored value is replaced with the supplied list.
          - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is the
            canonical way to remove all entries from a list. `null` is not accepted.
      minProperties: 1
      properties:
        allowed_domains:
          type: array
          items:
            type: string
          description: |
            Exact domain ("api.example.com") or wildcard prefix ("*.example.com").
            Wildcards must have at least 2 labels in the suffix.
            Pass `[]` to clear all allowed domains.
        allowed_ips:
          type: array
          items:
            type: string
          description: |
            CIDR-notation IP ranges, e.g. "10.0.0.0/24".
            Pass `[]` to clear all allowed IPs.
    SecureProxySummary:
      type: object
      required:
      - id
      - name
      - enabled
      - vpc_mode
      - integration_id
      - integration_name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        enabled:
          type: boolean
        vpc_mode:
          type: string
          enum:
          - static_ip
          - secure_link
        allowed_domains:
          type: array
          items:
            type: string
        allowed_ips:
          type: array
          items:
            type: string
        integration_id:
          type: string
          format: uuid
        integration_name:
          type: string
    SecureProxyRequest:
      type: object
      required:
      - url
      - method
      - integration_id
      properties:
        integration_id:
          type: string
          format: uuid
          description: Integration ID that owns the secure_proxy use case
        use_case_id:
          type: string
          format: uuid
          description: Use case ID (provide either use_case_id or use_case_slug)
        use_case_slug:
          type: string
          description: Use case slug (provide either use_case_id or use_case_slug)
        url:
          type: string
          format: uri
          description: Target URL to proxy the request to
        method:
          type: string
          enum:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
          description: HTTP method
        headers:
          type: object
          additionalProperties:
            type: string
          description: Request headers to forward
        body:
          description: Request body (string or JSON)
        response_type:
          type: string
          enum:
          - json
          - binary
          default: json
          description: Expected response type
    SecureProxyResponse:
      type: object
      properties:
        status_code:
          type: integer
          description: HTTP status code from the upstream response
        headers:
          type: object
          additionalProperties:
            type: string
          description: Response headers from upstream
        body:
          description: Response body from upstream. When `status_code` is 502 and the target never produced an HTTP response (TLS/connection/DNS failure or timeout), this is a `SecureProxyUpstreamError` describing the underlying cause.
    SecureProxyUpstreamError:
      type: object
      description: Error payload returned when epilot could not obtain an HTTP response from the proxied target. The failure is epilot-generated (HTTP 502) but the cause is usually remote-side; `code`/`reason` make that attributable.
      required:
      - message
      properties:
        message:
          type: string
          description: Short error category.
          enum:
          - Upstream network error
          - Upstream error
        code:
          type: string
          description: Underlying Node.js/axios error code when available (e.g. `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`).
        reason:
          type: string
          description: Human-readable explanation, present only for well-known codes (TLS/certificate, DNS and connection failures).
    ManagedCallOperationConfig:
      type: object
      required:
      - operation
      description: Configuration for managed_call use cases. Defines a single API operation with JSONata mapping.
      properties:
        operation:
          $ref: '#/components/schemas/ManagedCallOperation'
        request_mapping:
          type: string
          description: JSONata expression for outbound body transformation
        response_mapping:
          type: string
          description: JSONata expression for inbound response transformation
        inbound_use_case_slug:
          type: string
          description: |
            Slug of the inbound use case to route responses to for async entity processing.
            When set, the managed call response is queued to the inbound pipeline and processed
            using the referenced inbound use case's mapping configuration.
    ManagedCallOperation:
      type: object
      required:
      - method
      - path
      description: HTTP operation configuration for managed calls
      properties:
        method:
          type: string
          enum:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
        path:
          type: string
          description: URL path template with {{variable}} interpolation
        headers:
          type: object
          description: Custom HTTP headers for the request. Values support {{variable}} interpolation from the request payload and {{env.VAR}} references for environment variables.
          additionalProperties:
            type: string
        query_params:
          type: object
          description: Query parameters for the request. Values support {{variable}} interpolation from the request payload.
          additionalProperties:
            type: string
    ManagedCallExecuteRequest:
      type: object
      required:
      - integration_id
      properties:
        integration_id:
          type: string
          format: uuid
          description: Integration ID
        payload:
          type: object
          additionalProperties: true
          description: Request payload for the managed call operation
        correlation_id:
          type: string
          description: Correlation ID for tracing related events (auto-generated if not provided)
    ManagedCallExecuteResponse:
      description: |
        The response from a managed call execution.
        On success, returns the JSONata-mapped response data directly (no wrapper).
        The shape is entirely defined by your response_mapping JSONata expression.
        If no response_mapping is configured, returns the raw external API response.
        Check the X-Inbound-Event-Id header for inbound pipeline tracking when inbound routing is configured.
      additionalProperties: true
    ManagedCallErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              description: Error code (e.g., EXTERNAL_API_ERROR, SSRF_BLOCKED, MANAGED_CALL_EXECUTION_ERROR)
            message:
              type: string
              description: Human-readable error message
            details:
              type: object
              additionalProperties: true
              description: Additional error details (e.g., status code and body for external API errors)
    TypeAnnotations:
      type: object
      description: Developer-provided type annotations for a use case's request and response fields
      properties:
        request:
          type: object
          description: Type annotations for request fields, keyed by dot-path (e.g., "vendors[].id" -> "string")
          additionalProperties:
            type: string
        response:
          type: object
          description: Type annotations for response fields
          additionalProperties:
            type: string
    TypeDescriptor:
      type: object
      description: Describes the inferred type shape of a JSONata expression
      required:
      - kind
      properties:
        kind:
          type: string
          enum:
          - object
          - array
          - string
          - number
          - boolean
          - 'null'
          - unknown
          - union
        properties:
          type: object
          description: For kind=object, the properties and their type descriptors
          additionalProperties:
            $ref: '#/components/schemas/TypeDescriptor'
        items:
          description: For kind=array, the type of array items
          allOf:
          - $ref: '#/components/schemas/TypeDescriptor'
        source:
          type: string
          description: For kind=unknown, the JSONata path that produced this value
        variants:
          type: array
          description: For kind=union, the variant types
          items:
            $ref: '#/components/schemas/TypeDescriptor'
    UseCaseTypePreview:
      type: object
      description: Scaffolded type descriptors for a single use case
      required:
      - slug
      - request_shape
      - response_shape
      properties:
        slug:
          type: string
        name:
          type: string
        request_shape:
          $ref: '#/components/schemas/TypeDescriptor'
        response_shape:
          $ref: '#/components/schemas/TypeDescriptor'
        existing_annotations:
          $ref: '#/components/schemas/TypeAnnotations'
    GenerateTypesPreviewResponse:
      type: object
      required:
      - use_cases
      properties:
        integration_name:
          type: string
        use_cases:
          type: array
          items:
            $ref: '#/components/schemas/UseCaseTypePreview'
        previous_version:
          type: string
          description: Previously generated version, if any
        suggested_version:
          type: string
          description: Suggested next version based on detected changes
        suggested_bump:
          type: string
          enum:
          - major
          - minor
        detected_changes:
          type: array
          items:
            type: object
            required:
            - slug
            - field
            - type
            - description
            properties:
              slug:
                type: string
              field:
                type: string
              type:
                type: string
                enum:
                - breaking
                - non-breaking
              description:
                type: string
    GenerateTypesRequest:
      type: object
      required:
      - package_name
      - version
      properties:
        package_name:
          type: string
          description: npm package name
          example: '@epilot/hems-cleverpv'
        version:
          type: string
          description: Package version
          example: 1.0.0
        description:
          type: string
        domain_package:
          type: string
          description: Domain standard package to extend
          example: '@epilot/hems'
        domain_map_name:
          type: string
          description: Name of the domain UseCaseMap to extend
          example: HemsUseCaseMap
        annotations:
          type: object
          description: Type annotations per use case slug
          additionalProperties:
            $ref: '#/components/schemas/TypeAnnotations'
    GenerateTypesResponse:
      type: object
      required:
      - package_name
      - version
      - files
      properties:
        package_name:
          type: string
        version:
          type: string
        files:
          type: object
          description: Generated file contents keyed by file path
          additionalProperties:
            type: string
        warnings:
          type: array
          items:
            type: string
    CommitTypesRequest:
      type: object
      required:
      - package_name
      - version
      properties:
        package_name:
          type: string
          description: npm package name
          example: '@epilot/hems-cleverpv'
        version:
          type: string
          description: Package version
          example: 1.0.0
        annotations:
          type: object
          description: Type annotations per use case slug
          additionalProperties:
            $ref: '#/components/schemas/TypeAnnotations'
    CommitTypesResponse:
      type: object
      required:
      - committed
      properties:
        committed:
          type: boolean
        warnings:
          type: array
          items:
            type: string
    FileProxyUseCaseConfiguration:
      type: object
      required:
      - steps
      description: |
        Configuration for file_proxy use cases. Defines how to authenticate and move files
        between epilot and an external document system, in either direction (see `direction`).

        **Download** (`direction: download`, the default) fetches a file from the external system
        and serves it to a browser. The download URL always requires `orgId`, `integrationId`, and
        either `useCaseSlug` (recommended) or `useCaseId` (legacy UUID) as query parameters.
        The `orgId` is included in the signed URL to establish organization context without
        requiring authentication. Additional use-case-specific parameters are declared in the
        `params` array. `response` is REQUIRED for download use cases.

        **Upload** (`direction: upload`) pushes epilot files to the external system. It is not
        reachable over the download endpoint; an outbound use case points at it via a `file_proxy`
        delivery, and this configuration owns everything about what gets sent: `fan_out` decides
        how many deliveries one event produces, and every step builds its own request body with
        `body_jsonata` — or leaves it empty to send the delivery's files unchanged.
        `upload` is REQUIRED and `response` MUST be omitted.

        Two expression languages, split by what they produce. **Handlebars composes strings**:
        `url` and `headers` on upload, plus a form-encoded, XML or plain-text `body` on
        download. **JSONata produces data**: `body_jsonata` and the per-step `enabled`.
        JSONata omits keys whose value is undefined, so an optional field needs no conditional
        guard — it is simply absent from the serialized body.

        Handlebars templates are rendered EXACTLY ONCE, against a single context holding `env`,
        `file_data`, `steps` and `auth_token`. Writing
        `{{ env.some_var }}` resolves it. The legacy `\{{ env.some_var }}` escape belongs to the
        two-pass renderer and is NOT rewritten here — it renders as the literal text
        `{{ env.some_var }}`, which is rejected (see below) rather than shipped.

        Two guards run on every rendered upload template, because single-pass rendering fails
        quietly by default. Both are terminal, and each names what to fix:

        - **residual `{{` after rendering** — a configuration still carrying the `\{{` escape.
          Rewrite it without the backslash.
        - **a referenced `env` key absent from the environment** — checked BEFORE the URL is
          parsed, because an empty value in host position turns
          `https://{{env.host}}/document/import` into `https:///document/import`, whose host
          then parses as `document`. Provision the environment variable. A key that exists and
          is legitimately empty is fine; only absence fails.

        OpenAPI 3.0 cannot express this conditional requiredness, so it is enforced by the
        server-side validator, which returns an explicit message naming the offending field.
      properties:
        direction:
          type: string
          enum:
          - download
          - upload
          default: download
          description: |
            Direction of file travel. `download` (default) pulls a file from the external system
            into epilot; `upload` pushes an epilot file out to the external system. Omitted means
            `download`, so every pre-existing configuration keeps its exact meaning.

            Note this is the direction of the FILE, not the epilot use-case type — an `upload`
            file_proxy use case is still a `file_proxy` use case, never an `outbound` one.
        upload:
          $ref: '#/components/schemas/FileProxyUploadConfig'
        fan_out:
          $ref: '#/components/schemas/FileProxyFanOutConfig'
        secure_proxy:
          description: |
            Optional secure proxy attachment for routing all outbound file proxy requests.
            Only `use_case_slug` is supported and the referenced secure_proxy use case
            must belong to the same integration.
          allOf:
          - $ref: '#/components/schemas/FileProxySecureProxyAttachment'
        auth:
          $ref: '#/components/schemas/FileProxyAuth'
        params:
          type: array
          items:
            $ref: '#/components/schemas/FileProxyParam'
          description: |
            Download-only. Additional use-case-specific parameters expected in the download URL
            query string (beyond the required orgId, integrationId, and useCaseSlug or useCaseId).
            Rejected when `direction` is `upload`.
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
            pattern: ^https?://
          description: |
            Download-only. Additional origins permitted to call /download for this use case
            (CORS, exact match). Portal origins are always allowed. Rejected when `direction`
            is `upload`.
        steps:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/FileProxyStep'
          description: |
            Ordered list of HTTP steps to execute. For `download` these retrieve the file; for
            `upload` they deliver it, each assembling its own request body — `body_jsonata` for
            JSON, `body` for anything else.
        response:
          $ref: '#/components/schemas/FileProxyResponseConfig'
        prevent_indirect_serving:
          type: boolean
          default: false
          description: |
            Download-only; rejected when `direction` is `upload`.
            When `true`, this use case is served via the streaming endpoint: mapped file URLs
            are built as `/stream/download`, files of any size are streamed inline over HTTP
            response streaming, and buffered `/download` requests for oversize files are
            307-redirected to `/stream`. Files never transit epilot's temporary S3 storage on
            the streaming path. Defaults to `false` (small files are served directly and large
            files are transparently served via a temporary S3 redirect).
    FileProxySecureProxyAttachment:
      type: object
      required:
      - use_case_slug
      additionalProperties: false
      properties:
        use_case_slug:
          type: string
          description: Slug of the secure_proxy use case in the same integration.
    FileProxyAuth:
      type: object
      required:
      - type
      - token_url
      - client_id
      - client_secret
      properties:
        type:
          type: string
          enum:
          - oauth2_client_credentials
          - oauth2_password
          description: Authentication type
        token_url:
          type: string
          description: Handlebars template for the OAuth2 token endpoint URL
        client_id:
          type: string
          description: Handlebars template for the OAuth2 client ID
        client_secret:
          type: string
          description: Handlebars template for the OAuth2 client secret
        scope:
          type: string
          description: Optional OAuth2 scope
        audience:
          type: string
          description: Handlebars template for the OAuth2 audience parameter
        resource:
          type: string
          description: Handlebars template for the OAuth2 resource parameter
        username:
          type: string
          description: Handlebars template for the OAuth2 resource owner username. Required when type is oauth2_password.
        password:
          type: string
          description: Handlebars template for the OAuth2 resource owner password. Required when type is oauth2_password.
        body_params:
          type: object
          additionalProperties:
            type: string
          description: Additional key-value pairs to include in the token request body. Values support Handlebars templates.
        headers:
          type: object
          additionalProperties:
            type: string
          description: Additional headers to include in the token request. Values support Handlebars templates.
        query_params:
          type: object
          additionalProperties:
            type: string
          description: Additional query parameters to append to the token URL. Values support Handlebars templates.
    FileProxyParam:
      type: object
      required:
      - name
      - required
      properties:
        name:
          type: string
          description: Parameter name as it appears in the query string
        required:
          type: boolean
          description: Whether this parameter is required
        description:
          type: string
          description: Human-readable description of the parameter
    FileProxyStep:
      type: object
      required:
      - url
      - method
      - response_type
      properties:
        url:
          type: string
          description: Handlebars template for the request URL
        method:
          type: string
          enum:
          - GET
          - POST
          - PUT
          - PATCH
          description: HTTP method
        headers:
          type: object
          additionalProperties:
            type: string
          description: Handlebars templates for request headers
        body_jsonata:
          type: string
          description: |
            Upload-only. JSONata producing the request body as DATA; the result is serialized to
            JSON and sent. Use this for every JSON body — it cannot emit malformed JSON, and it
            omits a key whose value is undefined instead of sending it empty, which is what makes
            optional fields work without a conditional guard.

            The evaluation root is the hydrated event, so `contact.customer_pin` and
            `ticket._purpose` are reachable directly, unprefixed. **Everything else is a
            `$`-prefixed binding**: `$file_data` (the files this delivery carries — see
            `FileProxyFanOutConfig` for what it holds in each mode), `$ack_id`, `$env`,
            `$steps` (results of the steps already executed, each
            `{statusCode, headers, body}`), `$germanDate(iso)` and `$now()`.
            Writing `file_data[0].filename` instead of `$file_data[0].filename` yields
            nothing — it reads a field named `file_data` on the event, which does not exist.

            Leave this empty to send the delivery's files exactly as they are: the single
            attachment object when fanning out, the whole array when not. No mapping is
            needed for the common case.

            Must evaluate to an object or an array. Anything else fails the delivery terminally
            with `MAPPING_EXPRESSION_FAILED`, naming the step.

            **Only `undefined` omits a key.** `null`, `""`, `false` and `0` are values and are
            all sent. Two traps follow from that, and one idiom avoids both:

            - `x ? $string(x)` omits the key when `x` is `0` or `""`, because JSONata reads both
              as false. A meter number of `"0"` would silently vanish.
            - `: undefined` is not a literal — JSONata has no `undefined` keyword, so it is a
              path lookup that happens to find nothing. Against an event that really does carry
              a field named `undefined`, it returns that field's value.

            Write the **two-arm ternary with no else branch**, which omits the key when the test
            is false: `{ "pin": $exists(contact.pin) ? $string(contact.pin) }`.

            Every `$` binding is checked at save time against the known set, so a mistyped
            binding (`$lookup.documentType` for `$file_data[0].filename`) is rejected rather
            than silently producing a missing key.

            Not accepted together with `body`, which the upload direction rejects outright.
          example: '{ "documentType": "Zählerstand", "fileName": $file_data[0].filename, "fileData": $file_data[0].base64, "pin": $exists(contact.customer_pin) ? $string(contact.customer_pin) }'
        enabled:
          type: string
          description: |
            Upload-only, OPTIONAL. JSONata returning a boolean, deciding whether this step
            runs at all. Absent means it runs.

            A false result is a BREAK: this step is skipped and so is every step after it,
            and the delivery is recorded as `skipped` rather than delivered or failed. It is
            acknowledged and never retried, and a `STEP_DISABLED` monitoring event is emitted
            at level `info` — a disabled step is the configuration working, not a fault.

            Reads the same bindings a body does, `$steps` included, so it can branch on what
            an earlier step returned. This is how a delivery is filtered out: with one
            delivery per attachment, a false result on the first step drops that file.

            An expression that throws, or returns a non-boolean, is a terminal
            `MAPPING_EXPRESSION_FAILED` instead — a broken predicate must not read as a
            deliberate skip.
          example: $file_data[0].mime_type = "application/pdf"
        body:
          type: string
          description: |
            Handlebars template for the request body (write methods only), for bodies that are
            NOT JSON — form-encoded, XML, plain text. For a JSON body use `body_jsonata`
            instead; it cannot produce malformed JSON.

            **Download-only.** The upload direction rejects it: use `body_jsonata`, or leave
            that empty to send the files unchanged.

            Rendered once, against the context described on `FileProxyUseCaseConfiguration`:
            `{{ params.* }}`, `{{ env.* }}`, `{{ steps.N.body }}`, `{{ auth_token }}`.

            If a JSON body is written here anyway, **route every user-controlled value through
            the `json` helper**: the template engine does not escape, so
            `"name":"{{ params.documentName }}"` produces invalid JSON the moment a value
            contains a quote. Write `"name": {{json params.documentName}}` instead — the helper
            emits the surrounding quotes itself and renders absent values as `null`.

            Mutually exclusive with `body_jsonata`, which is upload-only.
        response_type:
          type: string
          enum:
          - json
          - binary
          description: Expected response type
    FileProxyFanOutConfig:
      type: object
      required:
      - enabled
      description: |
        Whether one event produces one delivery per file, or a single delivery carrying all of
        them. The split is always over the event's `event_attachments` — there is no expression
        to write, because an upload only ever runs on events that declare that field.

        Each resulting delivery is fully independent: its own idempotency record, its own retry
        schedule, its own monitoring events. A four-file event can therefore end up three-of-four
        delivered, which is the honest state to report.

        The split is evaluated ONCE, when the event is enqueued, so item indices — and therefore
        idempotency keys — stay stable across retries.

        Either way, expressions read the same `$file_data` binding, so no expression needs to
        know which mode it runs in. An event carrying no attachments is recorded as `skipped`.
      properties:
        enabled:
          type: boolean
          description: |
            When true, one delivery per attachment, and `$file_data` holds exactly that one
            file. When false (or absent), the event produces exactly one delivery and
            `$file_data` holds every attachment on the event.

            An empty `event_attachments` produces no deliveries and one info-level
            `FAN_OUT_EMPTY`, which is the normal outcome for a catch-all subscription seeing an
            event with nothing to send.
    FileProxyUploadConfig:
      type: object
      description: |
        Upload-side settings for a file_proxy use case with `direction: upload`.
        The surrounding file_proxy configuration owns WHAT and HOW to send: `fan_out`,
        auth, and the steps with their `body_jsonata`. This nested object governs
        upload-specific limits and how the final external response is judged. The
        outbound mapping remains a pure pointer to the recipe (see `FileProxyDeliveryConfig`).
      properties:
        max_total_bytes:
          type: integer
          description: |
            Ceiling for ALL of a delivery's files together, in bytes. Capped by the platform
            limit, which it may lower but never raise.

            Only reachable with `fan_out.enabled: false`, where one delivery carries every
            attachment and the worker holds them all resident at once. base64 inflates each
            by about a third, so the combined figure is what matters rather than any single
            file's size.
          example: 26214400
        max_file_bytes:
          type: integer
          minimum: 1
          maximum: 104857600
          description: |
            Per-file ceiling for this use case, in bytes. Files above it fail terminally with
            `FILE_TOO_LARGE` before any bytes are fetched. Defaults to — and is clamped by — the
            platform ceiling of 100 MiB (104857600).
        max_delivery_attempts:
          type: integer
          minimum: 1
          maximum: 100
          default: 8
          description: |
            Maximum delivery attempts per item before the delivery is marked failed. Attempts
            are spaced by an exponential, jittered backoff. The default 8 attempts schedule at
            most 7 delays, totaling about 7 hours 40 minutes before jitter, so a normal ERP
            maintenance window does not immediately exhaust them.

            Lives here rather than on the outbound delivery because how hard to retry is a
            property of the transport — the same judgement as `max_file_bytes` — and the outbound
            delivery is a pure pointer.
    FileProxyResponseConfig:
      type: object
      description: |
        How to extract the file from the step results. REQUIRED when `direction` is `download`;
        rejected when `direction` is `upload` (an upload has no file to extract).
      required:
      - body
      - encoding
      properties:
        body:
          type: string
          description: JSONata expression to extract file content from step results
        encoding:
          type: string
          enum:
          - base64
          - binary
          description: Encoding of the extracted body
        filename:
          type: string
          description: JSONata expression to extract the filename
        content_type:
          type: string
          description: JSONata expression to extract the content type
    MappingSimulationRequest:
      type: object
      required:
      - mapping_configuration
      - object_type
      - format
      - payload
      properties:
        mapping_configuration:
          oneOf:
          - $ref: '#/components/schemas/IntegrationConfigurationV1'
          - $ref: '#/components/schemas/IntegrationConfigurationV2'
        object_type:
          type: string
          description: |
            Type of the object/event being mapped.
            For v1.0: must match a key in mapping_configuration.mapping.objects
            For v2.0: must match a key in mapping_configuration.mapping.events
        format:
          type: string
          enum:
          - json
          - xml
          default: json
          description: Format of the payload data
        payload:
          oneOf:
          - type: string
            description: The serialized object data payload (JSON, XML, etc.) as a string
          - type: object
            description: Direct JSON object (will be automatically serialized)
            additionalProperties: true
          description: The object data payload - can be either a serialized string or a direct JSON object
    MappingSimulationV2Request:
      type: object
      description: |
        Request for v2 mapping simulation. Uses the same configuration format stored in integration use case resources,
        making it easier to test configurations before saving them.
      required:
      - event_configuration
      - format
      - payload
      properties:
        event_configuration:
          $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
        format:
          type: string
          enum:
          - json
          - xml
          default: json
          description: Format of the payload data
        payload:
          oneOf:
          - type: string
            description: The serialized event data payload (JSON, XML, etc.) as a string
          - type: object
            description: Direct JSON object (will be automatically serialized)
            additionalProperties: true
          description: The event data payload - can be either a serialized string or a direct JSON object
    MappingSimulationResponse:
      type: object
      required:
      - entity_updates
      properties:
        entity_updates:
          type: array
          items:
            $ref: '#/components/schemas/EntityUpdate'
        meter_readings_updates:
          type: array
          items:
            $ref: '#/components/schemas/MeterReadingUpdate'
        prune_scope_updates:
          type: array
          description: >-
            Resolved prune scopes produced by entity mappings using `upsert-prune-scope-purge` / `upsert-prune-scope-delete`. Each entry describes the entities that would be deleted at runtime because they exist in scope but were not part
            of this payload.
          items:
            $ref: '#/components/schemas/EntityPruneScopeUpdate'
        meter_readings_prune_scope_updates:
          type: array
          description: Resolved prune scopes produced by meter reading mappings using `upsert-prune-scope`. Each entry describes the readings that would be deleted at runtime for a meter (+ counter) because they were not part of this payload.
          items:
            $ref: '#/components/schemas/MeterReadingPruneScopeUpdate'
        warnings:
          type: array
          description: Validation warnings about the configuration (e.g., unique_ids referencing non-indexed fields)
          items:
            $ref: '#/components/schemas/MappingSimulationWarning'
    MappingSimulationWarning:
      type: object
      required:
      - entity_schema
      - field
      - message
      properties:
        entity_schema:
          type: string
          description: The entity schema slug where the issue was found
        field:
          type: string
          description: The field name that caused the warning
        message:
          type: string
          description: Description of the validation issue
    DirectPayload:
      type: object
      description: |
        The payload of an ERP event routed to a `direct: true` use case: a versioned envelope of
        pre-mapped operations that skip the mapping engine. Sent as the `payload` of an
        `ErpEventV3` - either as a JSON object or as a JSON string (`format` must be `json`;
        XML is rejected for direct use cases).

        The schema is strict: unknown keys on the envelope or on any operation are rejected with
        an error naming the offending path. Attribute values, by contrast, pass through to the
        entity verbatim (except `$relation` / `$relation_ref` envelopes, which are validated and
        resolved).

        One event lands as one queue message, so the whole event (including this payload) must
        stay within the 256 KiB message budget. Identical consecutive payloads within 5 minutes
        can be deduplicated by the queue - use the event's `deduplication_id` deliberately when
        re-sending identical data.
      required:
      - version
      - operations
      properties:
        version:
          type: string
          description: |
            Version of the direct payload contract. Currently only `"1"` is supported; an
            unsupported version is rejected with `DIRECT_VERSION_UNSUPPORTED`. Deliberately not
            an enum: the constraint is enforced by the handler so the dry-run endpoint returns
            a structured `valid: false` verdict instead of a schema-level 400.
        operations:
          type: array
          description: |
            The operations to apply, in order. At most 100 operations per event (enforced by
            the handler, with a structured error naming the limit). Each item is either an
            entity operation (`type` omitted or `"entity"`) or a meter reading operation
            (`type: "meter_reading"`).
          items:
            anyOf:
            - $ref: '#/components/schemas/DirectEntityOperation'
            - $ref: '#/components/schemas/DirectMeterReadingOperation'
    DirectEntityOperation:
      type: object
      description: |
        A single pre-mapped entity operation: upserts, soft-deletes, or purges one entity,
        identified by its unique ids. Produces the same internal update as a mapped-mode entity
        configuration, so create-vs-update resolution, relation resolution, ordering, and
        monitoring behave identically.
      required:
      - entity_slug
      - unique_ids
      additionalProperties: false
      properties:
        type:
          type: string
          enum:
          - entity
          description: Operation discriminator. Optional - an operation without `type` is an entity operation.
        entity_slug:
          type: string
          minLength: 1
          description: Target entity schema slug (e.g. `contact`, `contract`).
          example: contract
        unique_ids:
          type: object
          minProperties: 1
          additionalProperties:
            oneOf:
            - type: string
            - type: number
          description: |
            Attribute values that uniquely identify this entity, as `{ attribute: value }`.
            Values may be strings or numbers (numbers are coerced to strings); values that are
            empty after trimming are rejected. Use `_id` as the sole key to reference an entity
            directly by its epilot id.

            Unlike mapped mode, the unique-id ATTRIBUTE NAMES are given directly - there is no
            field-to-attribute mapping step.
          example:
            contract_number: CTR-001
        unique_id_types:
          type: object
          additionalProperties:
            type: string
            enum:
            - email
            - phone
          description: |
            Optional per-field type hints for repeatable unique-id fields (`email` / `phone`),
            needed so lookups search the repeatable storage format correctly. When omitted, the
            server derives the types from the entity schema; explicit values always win over
            derivation.
        mode:
          type: string
          enum:
          - upsert
          - delete
          - purge
          default: upsert
          description: |
            Operation mode: `upsert` creates or updates the entity (default), `delete` soft
            deletes it, `purge` removes it permanently. For `delete` / `purge`, `attributes`
            may be omitted - only `unique_ids` are used to locate the entity.
        attributes:
          type: object
          description: |
            Attribute values to write, keyed by attribute name. Required when `mode` is `upsert`
            (an empty object is allowed); optional for `delete` / `purge`.

            Values are written verbatim - the server does not validate them against the entity
            schema (attributes not defined in the schema are stored but not indexed; the
            simulation endpoint surfaces warnings for unique-id fields missing from the schema).
            Two envelope forms receive special treatment: `$relation` values
            (`DirectRelationValue`) and `$relation_ref` values (`DirectRelationRefValue`) are
            validated and resolved to entity relations.
          additionalProperties:
            anyOf:
            - $ref: '#/components/schemas/DirectRelationValue'
            - $ref: '#/components/schemas/DirectRelationRefValue'
            - description: Any other JSON value - written to the entity attribute verbatim.
    DirectMeterReadingOperation:
      type: object
      description: |
        A single pre-mapped meter reading operation: upserts or deletes one reading on a meter
        (and optionally a specific counter), identified by their unique ids. Produces the same
        internal update as a mapped-mode meter reading configuration.
      required:
      - type
      - meter
      - attributes
      additionalProperties: false
      properties:
        type:
          type: string
          enum:
          - meter_reading
          description: Operation discriminator. Required for meter reading operations.
        meter:
          type: object
          required:
          - unique_ids
          additionalProperties: false
          description: Reference to the meter this reading belongs to.
          properties:
            unique_ids:
              type: object
              minProperties: 1
              additionalProperties:
                oneOf:
                - type: string
                - type: number
              description: 'Attribute values that uniquely identify the meter, as `{ attribute: value }`.'
              example:
                meter_number: M-001
        counter:
          type: object
          required:
          - unique_ids
          additionalProperties: false
          description: |
            Optional reference to the meter counter (register) the reading belongs to. Required
            when `reading_matching` is `strict-date`.
          properties:
            unique_ids:
              type: object
              minProperties: 1
              additionalProperties:
                oneOf:
                - type: string
                - type: number
              description: 'Attribute values that uniquely identify the counter, as `{ attribute: value }`.'
              example:
                obis_number: 1-0:1.8.0
        mode:
          type: string
          enum:
          - upsert
          - delete
          default: upsert
          description: 'Operation mode: `upsert` creates or updates the reading (default), `delete` removes it.'
        reading_matching:
          type: string
          enum:
          - external_id
          - strict-date
          description: |
            Strategy for matching this reading against existing readings. `external_id` (the
            default) matches by the reading's `external_id`; `strict-date` matches by meter +
            counter + direction + date and REQUIRES `counter`. Useful when readings originate in
            the portal and are echoed back by the ERP with truncated timestamps.
        attributes:
          type: object
          required:
          - external_id
          - timestamp
          - source
          - value
          description: |
            The reading data. Additional keys (e.g. `direction`, `reason`, `read_by`, `status`,
            `metadata`) pass through to the metering service verbatim.
          properties:
            external_id:
              oneOf:
              - type: string
              - type: number
              description: External identifier of the reading (numbers are coerced to strings).
            timestamp:
              type: string
              description: |
                When the reading was taken. ISO 8601 - either `YYYY-MM-DD` or
                `YYYY-MM-DDTHH:mm:ss` (optional fractional seconds and `Z` / `±HH:mm` offset).
                Other formats (e.g. `DD.MM.YYYY`, epoch numbers) are rejected.
              example: '2026-08-24T06:00:00Z'
            source:
              type: string
              enum:
              - ECP
              - ERP
              - '360'
              - journey-submission
              description: Origin system of the reading.
            value:
              oneOf:
              - type: number
              - type: string
              description: The reading value - a number, or a numeric string (coerced to a number).
    DirectRelationValue:
      type: object
      description: |
        An entity attribute value that establishes relations to other entities. Written on any
        attribute inside a direct entity operation's `attributes`.

        Two forms are accepted: a bare array of items (shorthand for `_set`, i.e. replace), or
        an object with EXACTLY ONE of the operations `_set` (replace), `_append` (merge,
        deduplicated by entity id), `_append_all` (merge without deduplication). No other
        operation keys are accepted.
      required:
      - $relation
      additionalProperties: false
      properties:
        $relation:
          oneOf:
          - type: array
            description: Shorthand for `_set` - replaces the attribute's relations with these items.
            items:
              $ref: '#/components/schemas/DirectRelationItem'
          - type: object
            description: Exactly one of `_set`, `_append`, `_append_all` must be given.
            additionalProperties: false
            properties:
              _set:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationItem'
              _append:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationItem'
              _append_all:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationItem'
    DirectRelationItem:
      type: object
      description: |
        One relation target: either an already-resolved reference (`entity_id`), or a lookup by
        unique ids (`schema` + `unique_ids`, resolved by the pipeline like any other unique-id
        lookup, including stub creation and retry when the target does not exist yet). The two
        forms are mutually exclusive. Use `_id` as the sole `unique_ids` key for a direct-id
        lookup.
      additionalProperties: false
      properties:
        entity_id:
          type: string
          description: Epilot entity id of the relation target. Must not be combined with `schema` / `unique_ids`.
        schema:
          type: string
          description: Entity schema slug of the relation target. Required when `entity_id` is not given.
          example: contact
        unique_ids:
          type: object
          minProperties: 1
          additionalProperties:
            oneOf:
            - type: string
            - type: number
          description: Attribute values that uniquely identify the target. Required when `entity_id` is not given.
          example:
            customer_number: CUST-12345
        unique_id_types:
          type: object
          additionalProperties:
            type: string
            enum:
            - email
            - phone
          description: Optional per-field type hints for repeatable unique-id fields, as on the entity operation.
        tags:
          type: array
          items:
            type: string
          description: Optional relation tags (e.g. `primary`, `billing`).
    DirectRelationRefValue:
      type: object
      description: |
        An entity attribute value that references an ITEM INSIDE a repeatable attribute of
        another entity - for example one address out of a contact's `address` list. The target
        entity is looked up by unique ids, then the item at `path` matching `value` is
        referenced (creating it when missing).

        The same operation envelope as `$relation` applies: a bare array is shorthand for
        `_set`, otherwise exactly one of `_set`, `_append`, `_append_all`.
      required:
      - $relation_ref
      additionalProperties: false
      properties:
        $relation_ref:
          oneOf:
          - type: array
            description: Shorthand for `_set`.
            items:
              $ref: '#/components/schemas/DirectRelationRefItem'
          - type: object
            description: Exactly one of `_set`, `_append`, `_append_all` must be given.
            additionalProperties: false
            properties:
              _set:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationRefItem'
              _append:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationRefItem'
              _append_all:
                type: array
                items:
                  $ref: '#/components/schemas/DirectRelationRefItem'
    DirectRelationRefItem:
      type: object
      description: One relation-ref target - the entity to look up, and the repeatable item to reference on it.
      required:
      - schema
      - unique_ids
      - path
      - value
      additionalProperties: false
      properties:
        schema:
          type: string
          minLength: 1
          description: Entity schema slug of the target entity.
          example: contact
        unique_ids:
          type: object
          minProperties: 1
          additionalProperties:
            oneOf:
            - type: string
            - type: number
          description: Attribute values that uniquely identify the target entity.
        unique_id_types:
          type: object
          additionalProperties:
            type: string
            enum:
            - email
            - phone
          description: Optional per-field type hints for repeatable unique-id fields.
        path:
          type: string
          minLength: 1
          description: The attribute on the TARGET entity that holds the repeatable array (e.g. `address`).
        value:
          description: The item to match (or create) at `path` - e.g. an address object.
          example:
            street: Main Street
            city: Berlin
    DirectSimulationRequest:
      type: object
      description: |
        Request for a direct-mode dry run: the `direct: true` use case configuration to test
        against, and the payload to validate and translate.
      required:
      - event_configuration
      - payload
      properties:
        event_configuration:
          allOf:
          - $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
          description: |
            The direct use case configuration to simulate against. `direct` must be `true`;
            `entities` acts as the optional allowlist exactly as at runtime. This is the same
            configuration format stored on the inbound use case resource, so a configuration can
            be tested before saving it.
        payload:
          oneOf:
          - type: string
            description: The direct payload as a JSON string (as it would be sent in the event's `payload`).
          - $ref: '#/components/schemas/DirectPayload'
          description: The direct payload to validate - a `DirectPayload` object, or its JSON string form.
    DirectSimulationResponse:
      type: object
      description: |
        Result of a direct-mode dry run. `valid: false` responses list EVERY violation found
        across all operations in `errors`. `valid: true` responses preview the internal updates
        the pipeline would apply, plus non-blocking `warnings`.
      required:
      - valid
      - errors
      properties:
        valid:
          type: boolean
          description: Whether the payload would be accepted by the ingest endpoint.
        errors:
          type: array
          description: All validation errors found. Empty when `valid` is `true`.
          items:
            $ref: '#/components/schemas/DirectSimulationError'
        warnings:
          type: array
          description: |
            Non-blocking findings - e.g. unique-id fields not defined in the target entity
            schema (lookups would never match), or a failed server-side unique-id type
            derivation.
          items:
            $ref: '#/components/schemas/MappingSimulationWarning'
        entity_updates:
          type: array
          description: |
            The translated internal entity updates, exactly as the pipeline would process them.
            Only present when `valid` is `true`.
          items:
            allOf:
            - $ref: '#/components/schemas/EntityUpdate'
            - type: object
              properties:
                unique_identifiers_metadata:
                  type: object
                  description: |
                    Per-field metadata for repeatable unique-id fields - explicit
                    `unique_id_types` merged with server-side schema derivation. Absent when
                    no field resolved to a repeatable type.
                  additionalProperties:
                    type: object
                    properties:
                      fieldType:
                        type: string
                        enum:
                        - email
                        - phone
                      index:
                        type: integer
        meter_reading_updates:
          type: array
          description: |
            The translated internal meter reading updates. Only present when `valid` is `true`.
          items:
            allOf:
            - $ref: '#/components/schemas/MeterReadingUpdate'
            - type: object
              properties:
                _config:
                  type: object
                  description: Per-reading pipeline configuration carried by the operation.
                  properties:
                    reading_matching:
                      type: string
                      enum:
                      - external_id
                      - strict-date
    DirectSimulationError:
      type: object
      description: One validation error found during a direct-mode dry run.
      required:
      - code
      - message
      properties:
        code:
          type: string
          enum:
          - DIRECT_PAYLOAD_INVALID
          - DIRECT_VERSION_UNSUPPORTED
          - DIRECT_ENTITY_NOT_ALLOWED
          description: |
            Error class, matching the monitoring codes the ingest pipeline emits:
            `DIRECT_PAYLOAD_INVALID` (schema violation, JSON parse failure, or XML format),
            `DIRECT_VERSION_UNSUPPORTED` (unknown payload `version`),
            `DIRECT_ENTITY_NOT_ALLOWED` (entity or unique-id keys not permitted by the use
            case's allowlist).
        message:
          type: string
          description: Human-readable description, including the path of the offending element.
        operation_index:
          type: integer
          description: Zero-based index of the operation the error refers to, when attributable to one.
    EntityUpdate:
      type: object
      required:
      - entity_slug
      - unique_identifiers
      - attributes
      - mode
      properties:
        entity_slug:
          type: string
          description: The entity type slug
        unique_identifiers:
          type: object
          description: Unique identifier mappings for this entity
          additionalProperties: true
        attributes:
          type: object
          description: Mapped attribute values
          additionalProperties: true
        pricing:
          type: object
          description: Present when the entity mapping has a `pricing` block. Echoes the pricing configuration and the data extracted for it, so mapping authors can see what would be sent to the pricing service.
          required:
          - config
          - data
          properties:
            config:
              type: object
              additionalProperties: true
              description: The pricing configuration from the entity mapping
            data:
              type: array
              description: The pricing input data extracted from the payload
              items:
                type: object
                additionalProperties: true
        mode:
          type: string
          enum:
          - upsert
          - delete
          - purge
          description: >-
            Effective operation mode applied to this entity at runtime. `upsert-prune-scope-purge` / `upsert-prune-scope-delete` configurations report `upsert` here, because the individual entities in the payload are upserted — the
            destructive part of those modes is reported separately in `prune_scope_updates`. For `delete` / `purge`, `attributes` are still mapped and returned but ignored at runtime: only `unique_identifiers` are used to locate the entity
            to remove.
    MeterReadingUpdate:
      type: object
      required:
      - meter
      - attributes
      - mode
      properties:
        meter:
          type: object
          required:
          - $entity_unique_ids
          properties:
            $entity_unique_ids:
              type: object
              additionalProperties: true
              description: Unique identifiers for the meter
        meter_counter:
          type: object
          properties:
            $entity_unique_ids:
              type: object
              additionalProperties: true
              description: Unique identifiers for the meter counter
        attributes:
          type: object
          additionalProperties: true
          description: >-
            Meter reading attributes. Required: external_id, timestamp, source, value. `timestamp` must be ISO 8601 — either `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm:ss` (with optional fractional seconds and optional `Z` / `±HH:mm` timezone
            offset); non-ISO formats (e.g. `DD.MM.YYYY` or epoch numbers) are rejected and must be converted upstream via a `jsonataExpression` (e.g. `$fromMillis(...)`). Date-only values are normalized to midnight UTC and offset-less
            date-times are anchored to UTC before being forwarded to the metering API. `source` must be one of: ECP, ERP, 360, journey-submission. `reason` (optional) must be one of: regular, irregular, last, first, meter_change,
            contract_change, meter_adjustment (or empty/null).
        mode:
          type: string
          enum:
          - upsert
          - delete
          description: Effective operation mode applied to this reading at runtime. `upsert-prune-scope` configurations report `upsert` here — their destructive part is reported separately in `meter_readings_prune_scope_updates`.
    EntityPruneScopeUpdate:
      type: object
      description: A resolved entity prune scope. At runtime, entities of `entity_slug` found within `scope` whose unique identifiers are not listed in `keep_unique_ids` are removed using `deletion_mode`.
      required:
      - entity_slug
      - scope
      - keep_unique_ids
      - deletion_mode
      properties:
        entity_slug:
          type: string
          description: The entity type that would be pruned
        scope:
          type: object
          description: The scope configuration resolved against the payload
          required:
          - scope_mode
          properties:
            scope_mode:
              type: string
              enum:
              - relations
              - query
              description: How the entities in scope are found
            schema:
              type: string
              description: Schema of the related entity defining the scope (`relations` scope_mode only)
            unique_ids:
              type: object
              additionalProperties: true
              description: Resolved unique identifiers of the related scope entity (`relations` scope_mode only)
            query:
              type: object
              additionalProperties: true
              description: Resolved query parameters used to find entities in scope (`query` scope_mode only)
        keep_unique_ids:
          type: array
          description: Unique identifiers of the entities upserted by this payload. Everything else found in scope would be deleted.
          items:
            type: object
            additionalProperties: true
        deletion_mode:
          type: string
          enum:
          - delete
          - purge
          description: '`delete` soft deletes the pruned entities, `purge` removes them permanently'
    MeterReadingPruneScopeUpdate:
      type: object
      description: A resolved meter reading prune scope. At runtime, readings of the given meter (+ counter) whose external ids are not listed in `keep_external_ids` are deleted.
      required:
      - meter
      - keep_external_ids
      properties:
        meter:
          type: object
          required:
          - $entity_unique_ids
          properties:
            $entity_unique_ids:
              type: object
              additionalProperties: true
              description: Unique identifiers for the meter
        meter_counter:
          type: object
          properties:
            $entity_unique_ids:
              type: object
              additionalProperties: true
              description: Unique identifiers for the meter counter
        keep_external_ids:
          type: array
          description: External ids of the readings present in this payload. All other readings in scope would be deleted. An empty array means every reading in scope would be deleted.
          items:
            type: string
        source:
          type: string
          description: When set, only readings with this source are eligible for pruning (e.g. `ERP`)
    IntegrationConfigurationV1:
      type: object
      required:
      - mapping
      properties:
        version:
          type: string
          enum:
          - '1.0'
          default: '1.0'
          description: Mapping specification version
        mapping:
          type: object
          required:
          - objects
          properties:
            objects:
              type: object
              description: '[v1.0] Object type mappings'
              additionalProperties:
                $ref: '#/components/schemas/IntegrationObjectV1'
    IntegrationObjectV1:
      type: object
      required:
      - unique_ids
      - fields
      properties:
        unique_ids:
          type: object
          description: Mapping of entity types to their unique identifier field mappings
          additionalProperties:
            oneOf:
            - type: array
              items:
                type: string
                description: List of local keys (must be present under the mapping "fields"!)
            - type: object
              description: Map of remote keys to local entity keys
              additionalProperties:
                type: string
        fields:
          type: array
          description: Field mapping definitions
          items:
            $ref: '#/components/schemas/IntegrationFieldV1'
    IntegrationFieldV1:
      type: object
      required:
      - entity
      - attribute
      properties:
        entity:
          type: string
          description: Target entity slug
        attribute:
          type: string
          description: Target attribute name
        field:
          type: string
          description: Source field name (mutually exclusive with jsonataExpression)
        jsonataExpression:
          type: string
          description: JSONata expression for transformation (mutually exclusive with field)
    IntegrationConfigurationV2:
      type: object
      required:
      - version
      - mapping
      properties:
        version:
          type: string
          enum:
          - '2.0'
          description: Mapping specification version
        mapping:
          type: object
          required:
          - events
          properties:
            events:
              type: object
              description: '[v2.0] Event type mappings'
              additionalProperties:
                $ref: '#/components/schemas/InboundIntegrationEventConfiguration'
    OutboundMapping:
      type: object
      required:
      - name
      - enabled
      - delivery
      description: >-
        A mapping that delivers an event to an external system by one of three mechanisms — pushed to a webhook (with a JSONata payload transformation), made available on the pull-based poll queue (raw event payload, no transformation), or
        handed to a file_proxy use case that uploads files to an external document system (a pointer only; the referenced use case owns the payload and the fan-out)
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this mapping; generated by the API when omitted
        name:
          type: string
          description: Human-readable name for this mapping
          example: ERP Contract Sync
        jsonata_expression:
          type: string
          description: >-
            JSONata expression to transform the event payload. Required for webhook delivery, ignored for poll delivery, and rejected for file_proxy delivery — a file_proxy payload is built by the referenced use case's steps, so accepting
            an expression here would silently do nothing.
          example: '{ "id": entity._id, "customer": entity.customer_name }'
        enabled:
          type: boolean
          description: Whether this mapping is active
          default: true
        delivery:
          $ref: '#/components/schemas/DeliveryConfig'
        created_at:
          type: string
          format: date-time
          description: Timestamp when the mapping was created
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the mapping was last updated
    DeliveryConfig:
      description: >-
        Configuration for how the event should be delivered. webhook = push delivery via svc-webhooks (JSONata-transformed payload); poll = pull-based queue delivery where the consumer fetches items via the poll API (raw event payload);
        file_proxy = one push per event attachment to an external document system, through a file_proxy use case (JSONata-transformed payload carrying the file bytes)
      oneOf:
      - $ref: '#/components/schemas/WebhookDeliveryConfig'
      - $ref: '#/components/schemas/PollDeliveryConfig'
      - $ref: '#/components/schemas/FileProxyDeliveryConfig'
      discriminator:
        propertyName: type
        mapping:
          webhook: '#/components/schemas/WebhookDeliveryConfig'
          poll: '#/components/schemas/PollDeliveryConfig'
          file_proxy: '#/components/schemas/FileProxyDeliveryConfig'
    WebhookDeliveryConfig:
      type: object
      required:
      - type
      - webhook_id
      description: Push delivery of the transformed event to a webhook via svc-webhooks
      properties:
        type:
          type: string
          enum:
          - webhook
          description: Delivery mechanism type
        webhook_id:
          type: string
          description: Reference to the webhook configuration in svc-webhooks
        webhook_name:
          type: string
          description: Cached webhook name for display purposes
        webhook_url:
          type: string
          description: Cached webhook URL for display purposes
    PollDeliveryConfig:
      type: object
      required:
      - type
      description: Pull-based queue delivery. Items carry the raw standardized event-catalog payload; no JSONata mapping is applied in poll mode. Consumers fetch and acknowledge items via the poll API.
      properties:
        type:
          type: string
          enum:
          - poll
          description: Delivery mechanism type
        retention_days:
          type: integer
          minimum: 1
          maximum: 90
          default: 30
          description: How long undelivered queue items are retained before expiry
        poison_policy:
          type: string
          enum:
          - dead_letter
          - block
          default: dead_letter
          description: >-
            What happens when an item exhausts max_delivery_attempts: dead_letter routes the exhausted item to the dead-letter queue and advances past it so the stream keeps flowing; block halts the queue at that item until
            operator/consumer action removes it.
        max_delivery_attempts:
          type: integer
          minimum: 1
          maximum: 100
          default: 5
          description: Maximum delivery attempts before the poison_policy is applied
    FileProxyDeliveryConfig:
      type: object
      required:
      - type
      - use_case_slug
      description: |
        Push delivery to an external document system through a `file_proxy` use case.

        A pure pointer, deliberately. The outbound use case decides WHEN to deliver — its event
        name plus `event_filter` — and the referenced `file_proxy` use case decides WHAT and HOW:
        which items to fan out over (`fan_out`), what values to build (each
        step's `body_jsonata`), and the steps that send them. Keeping every file concern on the
        file_proxy side is what lets one upload recipe be shared by several event subscriptions
        without duplicating any of it.

        `jsonata_expression` on the mapping is NOT used for this delivery type and is rejected.

        The slug is resolved at delivery time, not at save time, so use cases can be saved in any
        order; a dangling reference surfaces as a `USE_CASE_NOT_FOUND` monitoring event naming
        the referencing mapping.
      properties:
        type:
          type: string
          enum:
          - file_proxy
          description: Delivery mechanism type
        use_case_slug:
          type: string
          description: |
            Slug of a `file_proxy` use case with `direction: upload` in the SAME integration.
          example: wemag_d3_document_upload
    EntitySyncStatusResponse:
      type: object
      required:
      - entity_id
      - sync_states
      properties:
        entity_id:
          type: string
          format: uuid
          description: The entity the sync states belong to
        sync_states:
          type: array
          description: One entry per integration that has processed an inbound event for the entity
          items:
            $ref: '#/components/schemas/EntitySyncState'
    EntitySyncState:
      type: object
      description: |
        Inbound sync state of one entity for one integration. Carries two
        independent groups: the entity-level fields (`last_synced_at`, …) and,
        on meter/meter_counter entities, the readings-level fields
        (`readings_last_synced_at`, …). Either group can be present alone — a
        meter whose readings sync via the ERP but whose entity was never
        itself inbound-mapped carries only the readings group.
      required:
      - entity_id
      - integration_id
      - entity_slug
      properties:
        entity_id:
          type: string
          format: uuid
          description: The entity ID
        integration_id:
          type: string
          format: uuid
          description: The integration that synchronized the entity
        entity_slug:
          type: string
          description: The entity schema slug
          example: contract
        use_case_id:
          type: string
          description: The inbound use case that last processed the entity
        last_synced_at:
          type: string
          format: date-time
          description: |
            Last time an inbound ERP event for this entity was processed —
            including no-op checks that changed nothing on the entity.
        last_changed_at:
          type: string
          format: date-time
          description: Last time an inbound ERP event actually changed the entity (create/patch/delete)
        last_operation:
          type: string
          enum:
          - create
          - patch
          - delete
          - no-op
          description: Outcome of the most recent inbound sync check
        last_event_id:
          type: string
          description: The event ID of the most recent inbound sync check
        readings_last_synced_at:
          type: string
          format: date-time
          description: |
            Last time meter readings for this meter/meter_counter entity were
            received and processed from the ERP. Only present on meter and
            meter_counter entities. The metering path cannot detect no-ops,
            so there is no separate readings changed-at.
        readings_last_operation:
          type: string
          enum:
          - upsert
          - delete
          description: Outcome of the most recent inbound readings sync
        readings_last_event_id:
          type: string
          description: The event ID of the most recent inbound readings sync
    OutboundStatusResponse:
      type: object
      required:
      - useCases
      properties:
        useCases:
          type: array
          items:
            $ref: '#/components/schemas/OutboundUseCaseStatus'
    OutboundUseCaseStatus:
      type: object
      required:
      - useCaseId
      - name
      - useCaseEnabled
      - status
      properties:
        useCaseId:
          type: string
          format: uuid
          description: Unique identifier for the use case
        name:
          type: string
          description: Human-readable name of the use case
        useCaseEnabled:
          type: boolean
          description: Whether the use case is enabled
        eventCatalogEvent:
          type: string
          description: The Event Catalog event name that triggers this outbound flow
          example: contract.created
        eventEnabled:
          type: boolean
          nullable: true
          description: Whether the event is enabled in Event Catalog. Null if the API is unreachable.
        webhooks:
          type: array
          items:
            $ref: '#/components/schemas/WebhookStatus'
        status:
          type: string
          enum:
          - ok
          - conflict
          - disabled
          description: |
            Overall status of the use case:
            - 'ok': Use case is enabled and all dependencies are properly configured
            - 'conflict': Use case has configuration issues (disabled events/webhooks while enabled)
            - 'disabled': Use case is disabled
        conflicts:
          type: array
          items:
            $ref: '#/components/schemas/OutboundConflict'
          description: List of detected conflicts, if any
        poll:
          $ref: '#/components/schemas/OutboundPollStatus'
        file_proxy:
          type: array
          items:
            $ref: '#/components/schemas/OutboundFileProxyTargetStatus'
          description: |
            One entry per file_proxy mapping on this use case. Present only on use cases with a
            file_proxy delivery mapping.
    OutboundFileProxyTargetStatus:
      type: object
      description: |
        Resolution state of one file_proxy mapping's referenced upload use case.

        Because the transport lives on a separate use case, edits to the endpoint or credentials
        do not appear in THIS use case's history. `target_updated_at` is the hook that lets an
        operator notice a target changed underneath a delivery that started failing.
      required:
      - mapping_id
      - use_case_slug
      - resolved
      properties:
        mapping_id:
          type: string
          description: The mapping holding this file_proxy delivery
        use_case_slug:
          type: string
          description: Slug of the referenced file_proxy use case
        resolved:
          type: boolean
          description: |
            Whether the slug currently resolves to an enabled `file_proxy` use case with
            `direction: upload` in this integration. References are resolved at delivery time,
            so `false` is a live warning rather than a save-time error.
        target_use_case_id:
          type: string
          format: uuid
          description: Id of the referenced use case — absent when unresolved
        target_enabled:
          type: boolean
          description: Whether the referenced use case is enabled — absent when unresolved
        target_updated_at:
          type: string
          format: date-time
          description: |
            When the referenced use case was last modified — absent when unresolved. Surfaced
            because a change here is invisible in this use case's own history diff.
        unresolved_reason:
          type: string
          enum:
          - not_found
          - wrong_type
          - wrong_direction
          - disabled
          description: Why the reference did not resolve — absent when `resolved` is true
    OutboundPollStatus:
      type: object
      description: |
        Queue/consumer health for a poll-mode use case. Present only on use
        cases with a poll delivery mapping — webhook-only use cases are
        unaffected. Depth/age/DLQ numbers are first-page approximations.
      required:
      - queue_depth
      - oldest_unconsumed_age_seconds
      - last_poll_at
      - last_ack_at
      - blocked
      - dlq_count
      properties:
        queue_depth:
          type: integer
          description: Unconsumed messages attributable to this use case (first-page approximation)
        oldest_unconsumed_age_seconds:
          type: integer
          nullable: true
          description: Age of the oldest unconsumed message in seconds — null when the queue is empty
        last_poll_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last successful poll lease — null before the first poll
        last_ack_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last committed acknowledgment — null before the first ack
        blocked:
          type: boolean
          description: |
            Whether the integration's outbound stream is currently halted by a
            blocked head message (poison_policy 'block'). Stream-level flag —
            the same value is reported on every poll use case of the
            integration, because the stream spans them.
        dlq_count:
          type: integer
          description: Dead-lettered messages of this use case awaiting redrive or expiry (first-page approximation)
    WebhookStatus:
      type: object
      required:
      - webhookId
      properties:
        webhookId:
          type: string
          description: Unique identifier for the webhook
        webhookName:
          type: string
          description: Human-readable name of the webhook
        enabled:
          type: boolean
          nullable: true
          description: Whether the webhook is enabled. Null if the API is unreachable.
    OutboundConflict:
      type: object
      required:
      - type
      - message
      properties:
        type:
          type: string
          enum:
          - event_disabled
          - all_webhooks_disabled
          - event_enabled_while_disabled
          - webhook_enabled_while_disabled
          - stream_blocked
          - dlq_items_present
          description: |
            Type of conflict:
            - 'event_disabled': Event catalog event is disabled while use case is enabled
            - 'all_webhooks_disabled': All webhooks are disabled while use case is enabled
            - 'event_enabled_while_disabled': Event is enabled while use case is disabled
            - 'webhook_enabled_while_disabled': A webhook is enabled while use case is disabled
            - 'stream_blocked': The outbound stream is halted by a poison message awaiting operator action or consumer ack (poll mode)
            - 'dlq_items_present': Dead-lettered messages await redrive or expiry (poll mode)
        webhookId:
          type: string
          description: Webhook ID (only present for webhook_enabled_while_disabled conflicts)
        message:
          type: string
          description: Human-readable description of the conflict
    PollOutboundMessagesRequest:
      type: object
      additionalProperties: false
      properties:
        limit:
          type: integer
          maximum: 100
          minimum: 1
          default: 10
          description: |
            Maximum number of messages to lease in this batch. The ~5.5 MB
            response cap may truncate the batch earlier when payloads are
            large — the leased run shrinks accordingly.
    OutboundMessage:
      type: object
      required:
      - id
      - lease_token
      - use_case_id
      - event_name
      - event_id
      - group
      - payload
      - enqueued_at
      properties:
        id:
          type: string
          description: Opaque message id (msg_…) — stable per message across leases
        lease_token:
          type: string
          description: Opaque lease token (lt_…) — echo back on ack
        use_case_id:
          type: string
          description: The poll-mode use case that produced this message
        event_name:
          type: string
          description: Standardized event name (e.g. contract.updated)
        event_id:
          type: string
          description: Unique id of the originating event
        group:
          type: string
          description: |
            Ordering group — messages sharing a group are strictly ordered,
            distinct groups are independent. Constant "0" in v1.
        payload:
          type: object
          additionalProperties: true
          description: The raw standardized event-catalog event, always inlined as-is
        enqueued_at:
          type: string
          format: date-time
          description: When the message was enqueued
    PollOutboundMessagesResponse:
      type: object
      required:
      - messages
      - visibility_timeout_seconds
      - has_more
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/OutboundMessage'
          description: The leased batch in strict stream order (empty when contended or drained)
        visibility_timeout_seconds:
          type: integer
          description: |
            Effective visibility timeout for this lease — a per-integration
            server-side setting (default 300 seconds)
        has_more:
          type: boolean
          description: Whether more messages are available beyond this batch
    AckOutboundMessagesRequest:
      type: object
      required:
      - acks
      properties:
        acks:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required:
            - id
            - lease_token
            properties:
              id:
                type: string
                description: The message id (msg_…) to acknowledge
              lease_token:
                type: string
                description: The lease token (lt_…) returned by the poll that leased this message
    AckResult:
      type: object
      required:
      - id
      - status
      properties:
        id:
          type: string
          description: The acknowledged message id
        status:
          type: string
          enum:
          - accepted
          - rejected
        reason:
          type: string
          enum:
          - stale_lease
          - out_of_order
          - not_found
          description: Rejection reason — present only when status is rejected
    AckOutboundMessagesResponse:
      type: object
      required:
      - results
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/AckResult'
          description: Per-id acknowledgement outcome
    OutboundDlqMessage:
      type: object
      required:
      - id
      - use_case_id
      - event_name
      - event_id
      - dead_lettered_at
      - delivery_attempts
      properties:
        id:
          type: string
          description: Opaque message id (msg_…) of the dead-lettered message
        use_case_id:
          type: string
          description: The poll-mode use case that produced this message
        event_name:
          type: string
          description: Standardized event name (e.g. contract.updated)
        event_id:
          type: string
          description: Unique id of the originating event
        enqueued_at:
          type: string
          format: date-time
          description: When the message was originally enqueued
        dead_lettered_at:
          type: string
          format: date-time
          description: When the message was dead-lettered
        delivery_attempts:
          type: integer
          description: Delivery attempts at the moment the message was dead-lettered
        reason:
          type: string
          description: Why the message was dead-lettered (policy or operator reason)
        expires_at:
          type: string
          format: date-time
          description: When the DLQ entry expires (retention window re-armed at dead-letter time)
    OutboundDlqListResponse:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/OutboundDlqMessage'
          description: Dead-lettered messages, oldest first
        next_token:
          type: string
          description: Opaque pagination token — present when more entries exist
    RedriveOutboundDlqRequest:
      type: object
      additionalProperties: false
      required:
      - ids
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
          description: Message ids (msg_…) of the DLQ entries to redrive
    RedriveOutboundDlqResult:
      type: object
      required:
      - id
      - status
      properties:
        id:
          type: string
          description: The requested message id
        status:
          type: string
          enum:
          - redriven
          - not_found
          description: |
            Outcome — `redriven` re-enqueued at the tail; `not_found` for
            unknown ids or entries concurrently redriven/expired
    RedriveOutboundDlqResponse:
      type: object
      required:
      - results
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/RedriveOutboundDlqResult'
          description: Per-id redrive outcome, input order preserved
    UnblockOutboundStreamRequest:
      type: object
      additionalProperties: false
      properties:
        reason:
          type: string
          maxLength: 500
          description: Operator reason recorded on the dead-lettered head
    UnblockOutboundStreamResponse:
      type: object
      required:
      - unblocked
      properties:
        unblocked:
          type: boolean
          description: Whether a blocked head was skipped (false = no-op, stream was not blocked)
        dead_lettered_id:
          type: string
          description: Message id of the skipped head — present only when unblocked is true
    RelationConfig:
      type: object
      required:
      - operation
      properties:
        operation:
          type: string
          enum:
          - _set
          - _append
          - _append_all
          description: |
            Relation operation:
            - '_set': Replace all existing relations with the specified items
            - '_append': Add new unique items to existing relations (deduplicates by entity_id)
            - '_append_all': Add all items to existing relations (no deduplication, allows duplicates)
        items:
          type: array
          description: Array of relation item configurations
          items:
            $ref: '#/components/schemas/RelationItemConfig'
        jsonataExpression:
          type: string
          description: JSONata expression that returns relation items array (alternative to 'items')
    RelationItemConfig:
      type: object
      required:
      - entity_schema
      - unique_ids
      properties:
        entity_schema:
          type: string
          description: Related entity schema
        _tags:
          type: array
          items:
            type: string
          description: Optional tags for this relation
        unique_ids:
          type: array
          description: Unique identifier mappings for the related entity
          items:
            $ref: '#/components/schemas/RelationUniqueIdField'
    RelationUniqueIdField:
      type: object
      required:
      - attribute
      properties:
        attribute:
          type: string
          description: Target attribute name in the related entity
        _type:
          $ref: '#/components/schemas/RepeatableFieldType'
        field:
          type: string
          description: Source field name from the event data
        jsonataExpression:
          type: string
          description: JSONata expression to compute the value
        constant:
          description: Constant value (any type)
    RelationRefsConfig:
      type: object
      description: |
        Configuration for relation references ($relation_ref).
        Relation references link to a specific item within a repeatable attribute on a related entity.
        Common use case: referencing a specific address within a contact's address list.
      required:
      - operation
      properties:
        operation:
          type: string
          enum:
          - _set
          - _append
          - _append_all
          description: |
            Relation reference operation:
            - '_set': Replace all existing relation_refs with the specified items
            - '_append': Add new unique items to existing relation_refs (deduplicates by entity_id + _id)
            - '_append_all': Add all items to existing relation_refs (no deduplication, allows duplicates)
        items:
          type: array
          description: Array of relation reference item configurations
          items:
            $ref: '#/components/schemas/RelationRefItemConfig'
        jsonataExpression:
          type: string
          description: JSONata expression that returns relation_ref items array (alternative to 'items')
    RelationRefItemConfig:
      type: object
      description: Configuration for a single relation reference item
      required:
      - entity_schema
      - unique_ids
      - path
      - value
      properties:
        entity_schema:
          type: string
          description: Schema of the related entity (e.g., "contact")
        unique_ids:
          type: array
          description: Unique identifier mappings for the related entity
          items:
            $ref: '#/components/schemas/RelationUniqueIdField'
        path:
          type: string
          description: Attribute path on the related entity (e.g., "address")
        value:
          $ref: '#/components/schemas/RelationRefValueConfig'
    RelationRefValueConfig:
      type: object
      description: Configuration for the value to set on the related entity's attribute
      required:
      - attribute
      properties:
        attribute:
          type: string
          description: Target attribute name on the related entity
        operation:
          type: string
          enum:
          - _set
          - _append
          - _append_all
          description: |
            Operation for the attribute value:
            - '_set': Replace the attribute value
            - '_append': Add new unique items (deduplicates)
            - '_append_all': Add all items (no deduplication)
        field:
          type: string
          description: Source field name from the event data
        jsonataExpression:
          type: string
          description: JSONata expression to compute the value
        constant:
          description: Constant value (any type)
    RepeatableFieldType:
      type: string
      enum:
      - email
      - phone
      description: |
        Type hint for repeatable fields that require special search handling.
        These fields are stored as arrays of objects (e.g., email: [{ email: "value" }]).
    ReplayEventsRequest:
      type: object
      required:
      - event_ids
      properties:
        event_ids:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100
          description: List of event IDs to replay. Maximum 100 events per request.
    QueryEventsRequest:
      type: object
      properties:
        event_id:
          type: string
          description: Filter by event ID
        event_type:
          type: string
          enum:
          - CREATE
          - UPDATE
          - DELETE
          description: Filter by event type
        correlation_id:
          type: string
          description: Filter by correlation ID
        object_type:
          type: string
          description: Filter by object type
        event_name:
          type: string
          description: Filter by event name (alias for object_type)
        use_case_id:
          type: string
          minLength: 1
          description: Filter by use case ID
        limit:
          type: integer
          description: Maximum number of results to return
          example: 25
          default: 25
          minimum: 1
          maximum: 25
        cursor:
          type: object
          description: Cursor for pagination. Use the next_cursor from the previous response to get the next page.
          properties:
            event_time:
              type: string
              format: date-time
              description: Timestamp from the last event in the previous page
              example: '2025-10-31T12:34:56Z'
            event_id:
              type: string
              description: Event ID from the last event in the previous page
              example: evt_1234567890abcdef
    QueryInboundMonitoringEventsRequest:
      type: object
      properties:
        use_case_id:
          type: string
          format: uuid
          description: Filter by use case ID
        event_type:
          type: string
          enum:
          - CREATE
          - UPDATE
          - DELETE
          - TRIGGER
          description: Filter by event type
        sync_type:
          type: string
          enum:
          - entity
          - meter_reading
          - webhook
          - api_deprecation
          description: Filter by sync type
        status:
          type: string
          enum:
          - success
          - error
          - skipped
          - warning
          description: Filter by processing status
        error_category:
          type: string
          enum:
          - validation
          - configuration
          - downstream_api
          - timeout
          - system
          description: Filter by error category (only applicable when status=error)
        correlation_id:
          type: string
          description: Filter by correlation ID
        object_type:
          type: string
          description: Filter by object type (e.g., 'contract', 'meter')
        event_name:
          type: string
          description: Filter by event name (alias for object_type)
        event_id:
          type: string
          description: Filter by event ID to find a specific event
        from_date:
          type: string
          format: date-time
          description: Filter events from this date (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: Filter events until this date (inclusive)
          example: '2025-01-31T23:59:59Z'
        limit:
          type: integer
          description: Maximum number of results to return
          example: 50
          default: 50
          minimum: 1
          maximum: 100
        cursor:
          type: object
          description: Cursor for pagination
          properties:
            completed_at:
              type: string
              format: date-time
              description: Timestamp from the last event in the previous page
            event_id:
              type: string
              description: Event ID from the last event in the previous page
    QueryAccessLogsRequest:
      type: object
      properties:
        token_id:
          type: string
          description: |
            Filter by a specific access token ID (e.g., 'api_5ZugdRXasLfWBypHi93Fk').
            Must be one of the access_token_ids linked to the integration.
            If omitted, returns logs for all access tokens linked to the integration.
          example: api_5ZugdRXasLfWBypHi93Fk
        service:
          type: string
          description: Filter by service name (e.g., 'entity', 'metering', 'submission-api')
          example: entity
        method:
          type: string
          enum:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
          - OPTIONS
          - HEAD
          description: Filter by HTTP method
        path:
          type: string
          description: Filter by request path (partial match)
          example: /v1/entity
        status:
          type: integer
          description: Filter by HTTP status code
          example: 200
        from_date:
          type: string
          format: date-time
          description: Filter logs from this date (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: Filter logs until this date (inclusive)
          example: '2025-01-31T23:59:59Z'
        limit:
          type: integer
          description: Maximum number of results to return
          example: 50
          default: 50
          minimum: 1
          maximum: 100
        cursor:
          type: object
          description: Cursor for pagination (infinite scroll)
          properties:
            timestamp:
              type: string
              format: date-time
              description: Timestamp from the last log entry in the previous page
            request_id:
              type: string
              description: Request ID from the last log entry in the previous page
    AccessLogEntry:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
          description: When the request was made
        environment:
          type: string
          description: Environment (e.g., 'dev', 'prod')
        service:
          type: string
          description: Service name (e.g., 'entity', 'metering')
        request_id:
          type: string
          description: Unique request identifier
        method:
          type: string
          description: HTTP method
        path:
          type: string
          description: Request path
        status:
          type: integer
          description: HTTP status code
        response_latency_ms:
          type: integer
          description: Response latency in milliseconds
        response_length:
          type: integer
          description: Response body length in bytes
        token_id:
          type: string
          description: Access token identifier
        org_id:
          type: string
          description: Organization ID
        origin:
          type: string
          description: Request origin header
        source_ip:
          type: string
          description: Client IP address
    GetMonitoringStatsRequest:
      type: object
      properties:
        from_date:
          type: string
          format: date-time
          description: Start date for statistics period (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: End date for statistics period (inclusive)
          example: '2025-01-31T23:59:59Z'
        inbound_group_by:
          type: array
          items:
            type: string
            enum:
            - use_case_id
            - sync_type
            - status
            - error_category
            - object_type
            - event_name
            - date
          description: Fields to group inbound statistics by
          example:
          - use_case_id
          - status
        outbound_group_by:
          type: array
          items:
            type: string
            enum:
            - event_name
            - status
            - webhook_config_id
            - date
          description: Fields to group outbound statistics by
          example:
          - event_name
          - status
    GetMonitoringTimeSeriesRequest:
      type: object
      required:
      - from_date
      - interval
      properties:
        from_date:
          type: string
          format: date-time
          description: Start date for the time series (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: End date for the time series (inclusive). Defaults to current time if not specified.
          example: '2025-01-31T23:59:59Z'
        interval:
          type: string
          enum:
          - 5m
          - 10m
          - 30m
          - 1h
          - 3h
          - 1d
          description: The time bucket interval for aggregation
          example: 1h
        direction:
          type: string
          enum:
          - inbound
          - outbound
          - both
          default: both
          description: Filter by event direction. Defaults to both.
          example: both
    TimeSeriesBucket:
      type: object
      required:
      - timestamp
      properties:
        timestamp:
          type: string
          format: date-time
          description: The start timestamp of the bucket
        inbound:
          type: object
          nullable: true
          description: Inbound event counts for this bucket. Null when direction is outbound.
          properties:
            success_count:
              type: integer
            error_count:
              type: integer
            warning_count:
              type: integer
            skipped_count:
              type: integer
            total_count:
              type: integer
        outbound:
          type: object
          nullable: true
          description: Outbound event counts for this bucket. Null when direction is inbound.
          properties:
            success_count:
              type: integer
            error_count:
              type: integer
            pending_count:
              type: integer
            total_count:
              type: integer
    QueryOutboundMonitoringEventsRequest:
      type: object
      properties:
        event_name:
          type: string
          description: Filter by event name (event_catalog_event). If not specified, returns events for all linked event names in the integration's outbound use cases.
          example: automation_flow_target
        status:
          type: string
          enum:
          - succeeded
          - failed
          - pending
          description: Filter by delivery status
        webhook_config_id:
          type: string
          description: Filter by webhook configuration ID
        from_date:
          type: string
          format: date-time
          description: Filter events from this date (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: Filter events until this date (inclusive)
          example: '2025-01-31T23:59:59Z'
        limit:
          type: integer
          description: Maximum number of results to return
          example: 50
          default: 50
          minimum: 1
          maximum: 100
        cursor:
          type: object
          description: Cursor for pagination
          properties:
            created_at:
              type: string
              format: date-time
              description: Timestamp from the last event in the previous page
            event_id:
              type: string
              description: Event ID from the last event in the previous page
    OutboundMonitoringEvent:
      type: object
      required:
      - org_id
      - event_id
      - event_name
      - status
      - created_at
      properties:
        org_id:
          type: string
          description: Organization ID
        event_id:
          type: string
          description: Unique event identifier
        event_name:
          type: string
          description: Event name (event_catalog_event)
        status:
          type: string
          enum:
          - succeeded
          - failed
          - pending
          description: Delivery status
        url:
          type: string
          description: Target URL
        http_method:
          type: string
          description: HTTP method used (e.g., POST)
        http_response:
          type: object
          description: HTTP response details (status_code, message, headers)
          additionalProperties: true
        webhook_config_id:
          type: string
          description: Webhook configuration ID
        metadata:
          type: object
          description: Additional metadata (webhook_id, organization details, correlation_id, etc.)
          additionalProperties: true
        execution_context:
          type: object
          description: Execution context (execution_arn, state_machine_arn, etc.)
          additionalProperties: true
        payload:
          type: object
          description: Payload that was sent
          additionalProperties: true
        created_at:
          type: string
          format: date-time
          description: When the event was created
        updated_at:
          type: string
          format: date-time
          description: When the event was last updated
    MonitoringStats:
      type: object
      required:
      - inbound
      - outbound
      properties:
        inbound:
          type: object
          description: Statistics for inbound (ERP sync) events
          required:
          - total_events
          - success_count
          - error_count
          - skipped_count
          properties:
            total_events:
              type: integer
              description: Total number of inbound events in the period
            total_correlations:
              type: integer
              description: Total number of unique correlation IDs
            success_count:
              type: integer
              description: Number of successful events
            error_count:
              type: integer
              description: Number of failed events
            skipped_count:
              type: integer
              description: Number of skipped events
            warning_count:
              type: integer
              description: Number of warning events
            success_rate:
              type: number
              format: float
              description: Success rate as percentage (0-100)
            last_error_at:
              type: string
              format: date-time
              nullable: true
              description: Timestamp of the most recent error
            breakdown:
              type: array
              description: Statistics breakdown by requested inbound_group_by fields
              items:
                type: object
                additionalProperties: true
        outbound:
          type: object
          description: Statistics for outbound (webhook delivery) events
          required:
          - total_events
          - success_count
          - error_count
          properties:
            total_events:
              type: integer
              description: Total number of outbound events in the period
            success_count:
              type: integer
              description: Number of successful deliveries
            error_count:
              type: integer
              description: Number of failed deliveries
            pending_count:
              type: integer
              description: Number of pending deliveries
            success_rate:
              type: number
              format: float
              description: Success rate as percentage (0-100)
            last_error_at:
              type: string
              format: date-time
              nullable: true
              description: Timestamp of the most recent error
            breakdown:
              type: array
              description: Statistics breakdown by requested outbound_group_by fields
              items:
                type: object
                additionalProperties: true
    InboundMonitoringEvent:
      type: object
      required:
      - org_id
      - event_id
      - object_type
      - sync_type
      - status
      - received_at
      - completed_at
      properties:
        org_id:
          type: string
          description: Organization ID
        event_id:
          type: string
          description: Unique event identifier
        correlation_id:
          type: string
          nullable: true
          description: Correlation ID for tracing related events
        integration_id:
          type: string
          nullable: true
          description: Integration ID
        use_case_id:
          type: string
          nullable: true
          description: Use case ID
        event_type:
          type: string
          enum:
          - CREATE
          - UPDATE
          - DELETE
          - TRIGGER
          description: Type of event (optional for V3 events)
        object_type:
          type: string
          description: Type of object being synced (e.g., 'contract', 'meter')
        sync_type:
          type: string
          enum:
          - entity
          - meter_reading
          - webhook
          - api_deprecation
          description: Type of sync operation
        status:
          type: string
          enum:
          - success
          - error
          - skipped
          - warning
          description: Processing status
        error_code:
          type: string
          nullable: true
          description: Error code (when status=error)
        error_message:
          type: string
          nullable: true
          description: Error message (when status=error)
        error_category:
          type: string
          nullable: true
          enum:
          - validation
          - configuration
          - downstream_api
          - timeout
          - system
          description: Error category (when status=error)
        processing_duration_ms:
          type: integer
          nullable: true
          description: Processing duration in milliseconds
        received_at:
          type: string
          format: date-time
          description: When the event was received
        completed_at:
          type: string
          format: date-time
          description: When processing completed
    QueryMonitoringEventsV2Request:
      type: object
      properties:
        use_case_id:
          type: string
          description: >
            Filter by use case ID (UUID). Empty string matches "General" events. Special value "__unknown__" matches events whose use_case_id does not belong to any currently configured use case for this integration (excludes "General"
            events).
        use_case_type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          description: Filter by use case type (replaces direction)
        level:
          type: string
          enum:
          - success
          - error
          - skipped
          - warning
          description: Filter by event level
        code:
          type: string
          description: Filter by taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502)
        event_id:
          type: string
          description: Filter by trigger/inbound event ID
        correlation_id:
          type: string
          description: Filter by correlation ID for tracing
        from_date:
          type: string
          format: date-time
          description: Filter events from this date (inclusive)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: Filter events until this date (inclusive)
          example: '2025-01-31T23:59:59Z'
        limit:
          type: integer
          description: Maximum number of results to return
          example: 50
          default: 50
          minimum: 1
          maximum: 100
        cursor:
          type: object
          description: Cursor for pagination (from previous response's next_cursor)
          properties:
            created_at:
              type: string
              format: date-time
            id:
              type: string
              format: uuid
    ExternalMonitoringSpan:
      type: object
      properties:
        correlation_id:
          type: string
          description: Trace id — links this span to the epilot trace. Unique per business operation. Required.
        level:
          type: string
          description: |
            Span outcome level — one of success | error | warning | info. Drives the server-assigned EXTERNAL_* code, coloring, and alerting. Required.
        use_case_slug:
          type: string
          description: >
            Business use case slug (e.g. "business_partner"). Resolved server-side against the integration's configured use cases to a use_case_id/use_case_type; stored as the grouping dimension. Portable — no epilot-internal UUIDs
            required.
        occurred_at:
          type: string
          format: date-time
          description: External clock time the span occurred; stored as created_at.
        message:
          type: string
          description: Human-readable line shown in the trace and event tables.
        detail:
          type: object
          additionalProperties: true
          nullable: true
          description: |
            Free-form context (step, http_status, reason, …). The place for external-system specificity — the taxonomy code stays clean (EXTERNAL_*).
      description: >
        A single monitoring span produced by an external system. `correlation_id`, `level`, `use_case_slug`, `occurred_at` and `message` are all required and `level` must be one of success|error|warning|info — but these are validated PER
        SPAN at ingest and reported in the per-item `results[]`, so one malformed span never fails the whole batch (the fields are intentionally not marked `required`/`enum` at the schema level to preserve that partial-acceptance behavior).
        The client does NOT send a `code` (server-assigned from `level`), a `source` (origin is the EXTERNAL_* code prefix), or span/event ids (the external system owns its own identity and dedup).
    IngestExternalMonitoringEventsRequest:
      type: object
      required:
      - events
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/ExternalMonitoringSpan'
          description: Batch of external monitoring spans (max 100 per request).
    IngestExternalMonitoringEventsResponse:
      type: object
      required:
      - accepted
      - rejected
      properties:
        accepted:
          type: integer
          description: Number of spans accepted and republished onto the monitoring bus.
        rejected:
          type: integer
          description: Number of spans rejected in validation. See `results` for reasons.
        results:
          type: array
          description: Per-span results; present for rejections in a mixed batch.
          items:
            type: object
            required:
            - index
            - status
            properties:
              index:
                type: integer
                description: Zero-based index of the span in the request `events` array.
              status:
                type: string
                enum:
                - accepted
                - rejected
              reason:
                type: string
                description: Rejection reason (present when status is rejected).
    MonitoringTraceResponse:
      type: object
      required:
      - correlation_id
      - status
      - span_count
      - truncated
      - spans
      properties:
        correlation_id:
          type: string
          description: The trace id these spans share.
        status:
          type: string
          enum:
          - success
          - error
          - warning
          - info
          description: Rolled-up status across all spans (error > warning > success > info).
        started_at:
          type: string
          format: date-time
          nullable: true
          description: created_at of the earliest span (null when the trace is empty).
        ended_at:
          type: string
          format: date-time
          nullable: true
          description: created_at of the latest returned span (null when the trace is empty).
        span_count:
          type: integer
          description: Number of spans RETURNED (== spans.length; capped).
        truncated:
          type: boolean
          description: |
            True when the trace has more spans than the cap; the returned window is the earliest spans, so ended_at/status reflect only that window.
        spans:
          type: array
          items:
            $ref: '#/components/schemas/MonitoringEventV2'
          description: |
            All spans sharing the correlation_id, chronological. External-origin spans carry an EXTERNAL_* code; epilot spans carry an epilot code or empty (success).
        inbound_event:
          type: object
          nullable: true
          additionalProperties: true
          description: The epilot inbound event "head" (raw payload), when present.
    MonitoringEventV2:
      type: object
      required:
      - id
      - org_id
      - integration_id
      - event_id
      - use_case_type
      - level
      - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique monitoring event ID
        org_id:
          type: string
          description: Organization ID
        integration_id:
          type: string
          description: Integration ID
        event_id:
          type: string
          description: Trigger/inbound event ID (groups related monitoring events)
        correlation_id:
          type: string
          description: Correlation ID for tracing. Empty when unavailable.
        use_case_id:
          type: string
          description: Use case ID. Empty for system-level events ("General").
        use_case_type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          - ''
          description: Use case type. Empty for system-level events.
        level:
          type: string
          enum:
          - success
          - error
          - skipped
          - warning
          description: Event outcome level
        code:
          type: string
          description: Taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Empty for success.
        message:
          type: string
          description: Human-readable message. Empty when not applicable.
        detail:
          type: object
          nullable: true
          additionalProperties: true
          description: Event-specific JSON data. Structure determined by `code`.
        created_at:
          type: string
          format: date-time
          description: When the monitoring event was created
    GetMonitoringStatsV2Request:
      type: object
      properties:
        from_date:
          type: string
          format: date-time
          description: Start of the time range
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: End of the time range
          example: '2025-01-31T23:59:59Z'
        use_case_type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          description: Filter stats by a single use case type
        use_case_types:
          type: array
          items:
            type: string
            enum:
            - inbound
            - outbound
            - file_proxy
            - managed_call
            - secure_proxy
          description: Filter stats to this set of use case types (matches any). Takes precedence over `use_case_type` when both are present. Used by the notification producers to scope alerts/digests to the integration's `monitoredUseCases`.
        group_by:
          type: string
          enum:
          - use_case_id
          - use_case_type
          - level
          - code
          - date
          description: Field to group the breakdown by
        source:
          type: string
          enum:
          - monitoring
          - incoming
          default: monitoring
          description: >
            Data source for the stats. "monitoring" (default) aggregates processed events from erp_monitoring_v2 — this counts every event produced throughout the processing tree (fan-out children, post-actions, relation resolutions, etc.).
            "incoming" counts only the initial inbound events actually received (distinct event_id from erp_incoming_events); only group_by=use_case_id is supported and status/level breakdown is not available for this source
            (success/error/warning/skipped counts are returned as 0).
    MonitoringStatsV2:
      type: object
      required:
      - total_events
      - success_count
      - error_count
      - warning_count
      - skipped_count
      properties:
        total_events:
          type: integer
          description: Total number of events in the period
        success_count:
          type: integer
          description: Number of successful events
        error_count:
          type: integer
          description: Number of error events
        warning_count:
          type: integer
          description: Number of warning events
        skipped_count:
          type: integer
          description: Number of skipped events
        ack_timeout_count:
          type: integer
          description: Number of ACK_TIMEOUT events (acknowledgement timed out)
        success_rate:
          type: number
          format: float
          description: Success rate as percentage (0-100)
        last_error_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent error
        breakdown:
          type: array
          description: Statistics breakdown by requested group_by field
          items:
            type: object
            additionalProperties: true
    GetMonitoringTimeSeriesV2Request:
      type: object
      required:
      - from_date
      - interval
      properties:
        from_date:
          type: string
          format: date-time
          description: Start of the time range (required)
          example: '2025-01-01T00:00:00Z'
        to_date:
          type: string
          format: date-time
          description: End of the time range (defaults to now)
          example: '2025-01-31T23:59:59Z'
        interval:
          type: string
          enum:
          - 5m
          - 10m
          - 30m
          - 1h
          - 3h
          - 1d
          description: Time bucket interval
        use_case_type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          description: Filter by use case type
        group_by:
          type: string
          enum:
          - use_case_type
          - use_case
          description: Field to group the bucket breakdown by
    TimeSeriesBreakdownItemV2:
      type: object
      required:
      - success_count
      - error_count
      - warning_count
      - skipped_count
      - total_count
      properties:
        use_case_type:
          type: string
          enum:
          - inbound
          - outbound
          - file_proxy
          - managed_call
          - secure_proxy
          description: Present when grouping by use_case_type
        use_case_id:
          type: string
          description: Present when grouping by use_case. Unknown historical IDs are collapsed into "__unknown__"
        success_count:
          type: integer
          description: Number of successful events in the breakdown item
        error_count:
          type: integer
          description: Number of error events in the breakdown item
        warning_count:
          type: integer
          description: Number of warning events in the breakdown item
        skipped_count:
          type: integer
          description: Number of skipped events in the breakdown item
        total_count:
          type: integer
          description: Total events in the breakdown item
    TimeSeriesBucketV2:
      type: object
      required:
      - timestamp
      - total_count
      properties:
        timestamp:
          type: string
          format: date-time
          description: Bucket start timestamp
        success_count:
          type: integer
          description: Number of successful events in the bucket
        error_count:
          type: integer
          description: Number of error events in the bucket
        warning_count:
          type: integer
          description: Number of warning events in the bucket
        skipped_count:
          type: integer
          description: Number of skipped events in the bucket
        total_count:
          type: integer
          description: Total events in the bucket
        breakdown:
          type: array
          description: Statistics breakdown by the requested group_by field for this bucket
          items:
            $ref: '#/components/schemas/TimeSeriesBreakdownItemV2'
    DocumentationPageSummary:
      type: object
      description: Documentation page metadata without the markdown content
      required:
      - id
      - integration_id
      - scope
      - title
      - created_at
      - updated_at
      properties:
        id:
          type: string
          description: '''general'' for the integration-wide page, otherwise the use case ID'
          example: general
        integration_id:
          type: string
          format: uuid
          readOnly: true
        scope:
          type: string
          enum:
          - integration
          - use_case
          description: Whether the page documents the whole integration or a single use case
        use_case_id:
          type: string
          format: uuid
          description: The linked use case. Only set when scope is use_case.
        title:
          type: string
          minLength: 1
          maxLength: 150
        created_at:
          type: string
          format: date-time
          readOnly: true
        created_by:
          type: string
          nullable: true
          description: User ID that created the page
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
        updated_by:
          type: string
          nullable: true
          description: User ID of the last edit
          readOnly: true
    DocumentationPage:
      description: A markdown documentation page of an integration
      allOf:
      - $ref: '#/components/schemas/DocumentationPageSummary'
      - type: object
        required:
        - content
        properties:
          content:
            type: string
            maxLength: 100000
            description: Markdown source of the page
    UpsertDocumentationPageRequest:
      type: object
      required:
      - title
      - content
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 150
        content:
          type: string
          maxLength: 100000
          description: Markdown source of the page
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    Unauthorized:
      description: Unauthorized request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    Forbidden:
      description: Forbidden - insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    Conflict:
      description: Conflict - the resource is not in a state that allows this operation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    InternalServerError:
      description: Internal Server Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseBase'
    QueryInboundMonitoringEventsResponse:
      description: Inbound monitoring events queried successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/InboundMonitoringEvent'
                description: List of inbound monitoring events
              next_cursor:
                type: object
                nullable: true
                description: Cursor to fetch the next page. Null if no more results.
                properties:
                  completed_at:
                    type: string
                    format: date-time
                  event_id:
                    type: string
              has_more:
                type: boolean
                description: Indicates if more results are available
    GetMonitoringStatsResponse:
      description: Monitoring statistics retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MonitoringStats'
    GetMonitoringTimeSeriesResponse:
      description: Time-series aggregated event counts retrieved successfully
      content:
        application/json:
          schema:
            type: object
            required:
            - interval
            - from_date
            - to_date
            - buckets
            properties:
              interval:
                type: string
                enum:
                - 5m
                - 10m
                - 30m
                - 1h
                - 3h
                - 1d
                description: The time bucket interval used for aggregation
              from_date:
                type: string
                format: date-time
                description: Start date of the time series
              to_date:
                type: string
                format: date-time
                description: End date of the time series
              buckets:
                type: array
                items:
                  $ref: '#/components/schemas/TimeSeriesBucket'
                description: List of time-series buckets with event counts
    QueryAccessLogsResponse:
      description: Access logs queried successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/AccessLogEntry'
                description: List of access log entries
              next_cursor:
                type: object
                nullable: true
                description: Cursor to fetch the next page. Null if no more results.
                properties:
                  timestamp:
                    type: string
                    format: date-time
                  request_id:
                    type: string
              has_more:
                type: boolean
                description: Indicates if more results are available
    QueryOutboundMonitoringEventsResponse:
      description: Outbound monitoring events queried successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/OutboundMonitoringEvent'
                description: List of outbound monitoring events
              next_cursor:
                type: object
                nullable: true
                description: Cursor to fetch the next page. Null if no more results.
                properties:
                  created_at:
                    type: string
                    format: date-time
                  event_id:
                    type: string
              has_more:
                type: boolean
                description: Indicates if more results are available
    ReplayEventsResponse:
      description: |
        Events replay initiated. Always 200 — per-event outcomes are reported in `results`, so inspect it (or compare `replayed` against the number of requested ids) rather than treating the status code as success.
      content:
        application/json:
          schema:
            type: object
            required:
            - replayed
            - results
            properties:
              replayed:
                type: integer
                description: Number of events actually queued for re-processing.
                example: 2
              results:
                type: array
                description: One entry per requested event id, in request order.
                items:
                  type: object
                  required:
                  - event_id
                  - status
                  properties:
                    event_id:
                      type: string
                      description: The requested (source) event ID.
                    status:
                      type: string
                      enum:
                      - success
                      - queued
                      - skipped
                      - ignored
                      - not_found
                      - error
                      description: >
                        Outcome for this event. `success`/`queued` means it was enqueued for re-processing. `not_found` means no inbound event with that ID exists for the organization. `skipped` means it was deduplicated, `ignored` means no
                        enabled use case matched it, and `error` means it could not be queued (see `message`).
                    replay_event_id:
                      type: string
                      description: |
                        The new event ID assigned to the replayed event. Use it to follow the replay in monitoring.
                    message:
                      type: string
                      description: Human-readable detail for this outcome.
              event_ids:
                type: array
                items:
                  type: string
                description: List of event IDs for which replay was requested
    ERPUpdatesResponse:
      description: Some events failed to process
      content:
        application/json:
          schema:
            type: object
            properties:
              results:
                type: array
                items:
                  type: object
                  required:
                  - event_id
                  - status
                  properties:
                    event_id:
                      type: string
                      description: ID of the processed event
                    status:
                      type: string
                      enum:
                      - success
                      - error
                      - skipped
                      - ignored
                      description: Processing status for the event (skipped indicates duplicate deduplication_id, ignored indicates unconfigured event)
                    message:
                      type: string
    TriggerWebhookResponse:
      description: Result of triggering an ERP synchronization event
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/TriggerWebhookResp'
    QueryEventsResponse:
      description: Events queried successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/ErpEvent'
                description: List of erp events
              next_cursor:
                type: object
                nullable: true
                description: Cursor to fetch the next page. Null if no more results.
                properties:
                  event_time:
                    type: string
                    format: date-time
                    example: '2025-10-31T12:34:56Z'
                  event_id:
                    type: string
                    example: evt_1234567890abcdef
              has_more:
                type: boolean
                description: Indicates if more results are available
                example: true
    QueryMonitoringEventsV2Response:
      description: Monitoring events queried successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/MonitoringEventV2'
                description: List of monitoring events
              next_cursor:
                type: object
                nullable: true
                description: Cursor to fetch the next page. Null if no more results.
                properties:
                  created_at:
                    type: string
                    format: date-time
                  id:
                    type: string
                    format: uuid
              has_more:
                type: boolean
                description: Indicates if more results are available
    GetMonitoringStatsV2Response:
      description: Monitoring statistics retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MonitoringStatsV2'
    GetMonitoringTimeSeriesV2Response:
      description: Time-series data retrieved successfully
      content:
        application/json:
          schema:
            type: object
            properties:
              interval:
                type: string
                enum:
                - 5m
                - 10m
                - 30m
                - 1h
                - 3h
                - 1d
                description: The bucket interval
              from_date:
                type: string
                format: date-time
              to_date:
                type: string
                format: date-time
              buckets:
                type: array
                items:
                  $ref: '#/components/schemas/TimeSeriesBucketV2'
    GetAssociatedMonitoringEventsResponse:
      description: Associated monitoring events for the given event_id
      content:
        application/json:
          schema:
            type: object
            properties:
              monitoring_events:
                type: array
                items:
                  $ref: '#/components/schemas/MonitoringEventV2'
                description: All monitoring events sharing this event_id, ordered by created_at ASC
              inbound_event:
                type: object
                nullable: true
                additionalProperties: true
                description: The original inbound event payload from erp_incoming_events, if available

