openapi: 3.0.3
info:
  title: Sprava Pro External Integration API
  version: "1.0.0"
  description: |
    Public API for external product integrations with SpravaPro.

    Connect companies, exchange accounting events (sales, payments, and related
    corrections), and validate your integration in a sandbox before going live.
  contact:
    name: Sprava Pro Platform
  license:
    name: Proprietary

servers:
  - url: https://api.spravapro.example
    description: Production API
  - url: http://localhost:8080
    description: Local development

tags:
  - name: ExternalConnections
    description: Company connection and authorization for external products
  - name: ExternalEvents
    description: Accounting event submission and status

security:
  - ProducerAppAuth: []

paths:
  /api/v1/external-connections/requests:
    post:
      tags: [ExternalConnections]
      operationId: createExternalConnectionRequest
      summary: Start external connection request
      description: |
        Server-to-server. Producer application authenticates; creates an
        ExternalConnectionRequest and returns authorization redirect material.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalConnectionRequestCreate'
            examples:
              fopUa:
                $ref: '#/components/examples/ConnectionRequestFopUa'
      responses:
        '201':
          description: Connection request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalConnectionRequestCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/external-connections/token:
    post:
      tags: [ExternalConnections]
      operationId: exchangeExternalConnectionToken
      summary: Exchange authorization code (or refresh) for connection tokens
      description: |
        Mandatory token endpoint after consent redirect.
        Token format is opaque; producers must not parse internals.
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalConnectionTokenExchange'
      responses:
        '200':
          description: Tokens issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalConnectionTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /api/v1/external-connections/{connection_id}:
    get:
      tags: [ExternalConnections]
      operationId: getExternalConnection
      summary: Read connection status
      description: Requires the connection.read scope on the connection access token.
      security:
        - ConnectionBearer: [connection.read]
      parameters:
        - $ref: '#/components/parameters/ConnectionId'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Connection snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalConnection'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/external-events:
    post:
      tags: [ExternalEvents]
      operationId: ingestExternalEvent
      summary: Ingest a single external business event
      description: |
        Accepts accounting events. Returns 202 when durably accepted.
      security:
        - ConnectionBearer: [external_events.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalEventEnvelope'
            examples:
              saleCompleted:
                $ref: '#/components/examples/SaleCompletedEnvelope'
      responses:
        '202':
          description: Durably accepted (processing may be async)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalEventAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Idempotent replay or hash conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalEventConflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/external-events/{event_id}:
    get:
      tags: [ExternalEvents]
      operationId: getExternalEventStatus
      summary: Read external event processing status / lineage façade
      description: Event status API.
      security:
        - ConnectionBearer: [external_events.status.read]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Event status snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalEventStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/external-events/batch:
    post:
      tags: [ExternalEvents]
      operationId: ingestExternalEventBatch
      summary: Batch ingest external events (v1 should-deliver)
      description: Optional batch ingest.
      security:
        - ConnectionBearer: [external_events.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalEventBatchRequest'
      responses:
        '202':
          description: Batch accepted (per-item results)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalEventBatchAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

components:
  securitySchemes:
    ProducerAppAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: ProducerApplication server credential (client credentials / signed app token).
    ConnectionBearer:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: ExternalConnection access token issued by `/external-connections/token`.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
      description: Client-generated idempotency key.
    CorrelationId:
      name: X-Correlation-Id
      in: header
      required: false
      schema:
        type: string
    ConnectionId:
      name: connection_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^conn_'
    EventId:
      name: event_id
      in: path
      required: true
      schema:
        type: string

  responses:
    BadRequest:
      description: Malformed request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing/invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Scope or tenant mismatch
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: Conflict / duplicate
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: Schema or business validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limited; Retry-After header required
      headers:
        Retry-After:
          description: Seconds (or HTTP-date) until the client may retry
          required: true
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  examples:
    ConnectionRequestFopUa:
      summary: UA FOP connection request
      value:
        producer:
          producer_id: orli
          installation_id: company_482
        initiator:
          external_user_id: user_918
          email: owner@example.com
          phone: '+380XXXXXXXXX'
          first_name: Іван
          last_name: Петренко
          locale: uk-UA
        organization:
          external_id: company_482
          country: UA
          legal_form: fop
          legal_name: ФОП Петренко Іван Іванович
          display_name: Beauty Studio
          tax_identifier:
            type: rnokpp
            value: 'XXXXXXXXXX'
          currency: UAH
          timezone: Europe/Kyiv
        requested_scopes:
          - connection.read
          - external_events.write
          - external_events.status.read
        redirect_uri: https://producer.example.com/spravapro/callback
        state: producer-generated-random-state
    SaleCompletedEnvelope:
      summary: sale.completed@1 envelope
      value:
        event_id: evt_01KEXAMPLE
        event_type: sale.completed
        event_version: 1
        occurred_at: '2026-08-14T18:42:13+03:00'
        source:
          producer_id: orli
          installation_id: company_482
        subject:
          type: sale
          external_id: visit_89127
        scope:
          external_organization_id: company_482
          external_location_id: location_7
        correlation_id: corr_example
        causation_id: null
        data:
          sale_external_id: sale_89127
          location:
            external_id: location_7
          currency: UAH
          total:
            amount: '1250.00'
            currency: UAH
          lines:
            - line_type: service
              item:
                external_id: svc_cut
              quantity:
                quantity: '1'
                unit: ea
              unit_price:
                amount: '1250.00'
                currency: UAH
              line_total:
                amount: '1250.00'
                currency: UAH
        metadata: {}

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Public error catalog code
              enum:
                - invalid_request
                - unauthorized
                - forbidden
                - source_mismatch
                - not_found
                - conflict
                - connection_inactive
                - validation_failed
                - unsupported_event_type
                - unsupported_event_version
                - redirect_uri_mismatch
                - invalid_grant
                - rate_limited
                - internal_error
            message:
              type: string
            details:
              type: object
              additionalProperties: true

    TaxIdentifier:
      type: object
      required: [type, value]
      properties:
        type:
          type: string
          enum: [edrpou, rnokpp]
        value:
          type: string

    ExternalConnectionRequestCreate:
      type: object
      required: [producer, initiator, organization, requested_scopes, redirect_uri, state]
      properties:
        producer:
          type: object
          required: [producer_id, installation_id]
          properties:
            producer_id:
              type: string
            installation_id:
              type: string
        initiator:
          type: object
          required: [external_user_id]
          properties:
            external_user_id:
              type: string
            email:
              type: string
              format: email
            phone:
              type: string
            first_name:
              type: string
            last_name:
              type: string
            locale:
              type: string
              example: uk-UA
        organization:
          type: object
          required: [external_id, country, legal_name, tax_identifier]
          properties:
            external_id:
              type: string
            country:
              type: string
              example: UA
            legal_form:
              type: string
            legal_name:
              type: string
            display_name:
              type: string
            tax_identifier:
              $ref: '#/components/schemas/TaxIdentifier'
            currency:
              type: string
              example: UAH
            timezone:
              type: string
              example: Europe/Kyiv
        requested_scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          minItems: 1
        redirect_uri:
          type: string
          format: uri
        state:
          type: string

    ExternalConnectionRequestCreated:
      type: object
      required: [request_id, status, authorization_url, state]
      properties:
        request_id:
          type: string
        status:
          $ref: '#/components/schemas/ConnectionStatus'
        authorization_url:
          type: string
          format: uri
        state:
          type: string
        expires_at:
          type: string
          format: date-time

    ExternalConnectionTokenExchange:
      type: object
      required: [grant_type]
      properties:
        grant_type:
          type: string
          enum: [authorization_code, refresh_token]
        code:
          type: string
          description: Required when grant_type=authorization_code
        refresh_token:
          type: string
          description: Required when grant_type=refresh_token
        redirect_uri:
          type: string
          format: uri
        client_id:
          type: string
        client_secret:
          type: string

    ExternalConnectionTokenResponse:
      type: object
      required: [connection_id, access_token, token_type, expires_in, scope]
      properties:
        connection_id:
          type: string
        access_token:
          type: string
        refresh_token:
          type: string
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          example: 3600
        scope:
          type: array
          items:
            $ref: '#/components/schemas/Scope'

    ConnectionStatus:
      type: string
      enum:
        - requested
        - awaiting_user_verification
        - awaiting_company_selection
        - awaiting_company_creation
        - awaiting_consent
        - active
        - suspended
        - revoked
        - expired
        - failed

    Scope:
      type: string
      enum:
        - connection.read
        - external_events.write
        - external_events.status.read
        - reference_data.read
        - reconciliation.read
        - documents.status.read
        - external_events.status.webhook

    ExternalConnection:
      type: object
      required: [id, producer_application_id, producer_installation_id, status, granted_scopes]
      properties:
        id:
          type: string
        producer_application_id:
          type: string
        producer_installation_id:
          type: string
        status:
          $ref: '#/components/schemas/ConnectionStatus'
        spravapro_company_id:
          type: string
          description: Server-side tenant binding; never selected by producer payload
        granted_scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
        created_by_user_id:
          type: string
          nullable: true
        consented_at:
          type: string
          format: date-time
          nullable: true
        suspended_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ExternalEventEnvelope:
      type: object
      required: [event_id, event_type, event_version, occurred_at, source, subject, data]
      description: |
        Event envelope. `data` MUST validate against the published
        JSON Schema for `event_type` + `event_version` (see `events.yaml` / `schemas/`).
      properties:
        event_id:
          type: string
          description: Stable across retries
        event_type:
          type: string
          example: sale.completed
        event_version:
          type: integer
          minimum: 1
          example: 1
        occurred_at:
          type: string
          format: date-time
        source:
          type: object
          required: [producer_id, installation_id]
          properties:
            producer_id:
              type: string
            installation_id:
              type: string
        subject:
          type: object
          required: [type, external_id]
          properties:
            type:
              type: string
            external_id:
              type: string
              description: Producer domain identity — not a SpravaPro id
        scope:
          type: object
          properties:
            external_organization_id:
              type: string
            external_location_id:
              type: string
          description: Traceability only — never selects the company tenant
        correlation_id:
          type: string
          nullable: true
        causation_id:
          type: string
          nullable: true
        data:
          type: object
          additionalProperties: true
        metadata:
          type: object
          additionalProperties: true
          description: Must not override accounting semantics

    ExternalEventAccepted:
      type: object
      required: [event_id, status, received_at]
      properties:
        event_id:
          type: string
        status:
          $ref: '#/components/schemas/ExternalEventProcessingStatus'
        received_at:
          type: string
          format: date-time
        duplicate:
          type: boolean
          default: false

    ExternalEventConflict:
      type: object
      required: [event_id, status]
      properties:
        event_id:
          type: string
        status:
          type: string
          enum: [duplicate, conflict]
        message:
          type: string

    ExternalEventProcessingStatus:
      type: string
      description: Processing states
      enum:
        - accepted
        - validated
        - needs_mapping
        - needs_configuration
        - processed
        - rejected
        - duplicate
        - conflict

    ExternalEventStatus:
      type: object
      required: [event_id, status, received_at]
      properties:
        event_id:
          type: string
        status:
          $ref: '#/components/schemas/ExternalEventProcessingStatus'
        received_at:
          type: string
          format: date-time
        processed_at:
          type: string
          format: date-time
          nullable: true
        result:
          type: object
          nullable: true
          properties:
            type:
              type: string
            id:
              type: string
            number:
              type: string
        mapping_block:
          type: object
          nullable: true
          additionalProperties: true
        configuration_block:
          type: object
          nullable: true
          additionalProperties: true
        lineage:
          type: object
          nullable: true
          additionalProperties: true

    ExternalEventBatchRequest:
      type: object
      required: [events]
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/ExternalEventEnvelope'

    ExternalEventBatchAccepted:
      type: object
      required: [results]
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ExternalEventAccepted'

