openapi: "3.1.0"
info:
  title: MiniApp — Incoming Events
  description: >
    Incoming events from the external application (MiniApp → Acloud).
    The external application sends a POST request to the hook endpoint.
  version: "2.0.0"

webhooks:

  message.new.personal:
    post:
      summary: New incoming message from user
      description: >
        Sent by the external application when a new message is received from a user.
        Also used as confirmation of chat initialization in response to the outgoing
        hook chat.init.personal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.new.personal
                payload:
                  $ref: '#/components/schemas/MessageNewPayload'
            examples:
              message_new:
                summary: New message
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_123
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Hi, I want to ask about my order
                    send_date: 1710752400
                    user_data:
                      name: John Smith
                      username: john
                      phone: "+79001234567"
                      email: john@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/john
              message_new_with_attachment:
                summary: New message with attachment
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_124
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Here is the screenshot you requested
                    send_date: 1710752500
                    user_data:
                      name: John Smith
                      username: john
                      phone: "+79001234567"
                      email: john@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/john
                    attachments:
                      - id: ext_att_1001
                        url: https://miniapp.example/files/1001
                        filename: screenshot.png
                        size: 204800
                        type: photo
              message_new_with_failed_attachments:
                summary: >
                  New message with partial attachment delivery declaration:
                  the external application delivered some files and notifies Acloud
                  that some files were not included in the attachments array.
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_125
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Sending photo
                    send_date: 1710752600
                    user_data:
                      name: John Smith
                      username: john
                      phone: "+79001234567"
                    attachments:
                      - id: ext_att_1002
                        url: https://miniapp.example/files/1002
                        filename: photo_1.jpg
                        size: 524288
                        type: photo
                    has_failed_attachments: true
                    failed_attachments_count: 4
      responses:
        "200":
          description: Event processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Validation error (invalid format or missing required fields)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.delete.personal:
    post:
      summary: Message deleted by user
      description: >
        Sent by the external application when a message is deleted by a user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.delete.personal
                payload:
                  $ref: '#/components/schemas/MessageDeletedPayload'
            examples:
              message_deleted:
                summary: Message deletion
                value:
                  method: message.delete.personal
                  payload:
                    external_message_id: msg_123
                    deleted_date: 1710752400
      responses:
        "200":
          description: Event processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Validation error (invalid format or missing required fields)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Message with the specified external_message_id not found in the scope of the webhook
            owner bot (cross-bot, does not exist, or the message is an outgoing manager message
            and cannot be modified externally).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.edit.personal:
    post:
      summary: Message edited by user
      description: >
        Sent by the external application when a message is edited by a user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.edit.personal
                payload:
                  $ref: '#/components/schemas/MessageEditedPayload'
            examples:
              message_edited:
                summary: Message editing
                value:
                  method: message.edit.personal
                  payload:
                    external_message_id: msg_123
                    edited_date: 1710752400
                    new_text: Corrected message text
      responses:
        "200":
          description: Event processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Validation error (invalid format or missing required fields)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Message with the specified external_message_id not found in the scope of the webhook
            owner bot (cross-bot, does not exist, or the message is an outgoing manager message
            and cannot be modified externally).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  error:
    post:
      summary: Error notification for outgoing hook processing
      description: >
        The external application sends this hook if it could not process an outgoing request
        from Acloud (e.g., user not found in the system). The event_id field is returned
        unchanged from the event_id field of the outgoing hook payload.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - error
                payload:
                  $ref: '#/components/schemas/ErrorPayload'
            examples:
              error_user_not_found:
                summary: User not found (message.new.personal error)
                value:
                  method: error
                  payload:
                    message: "User not found"
                    event_id: '550e8400-e29b-41d4-a716-446655440000:79000'
              error_chat_init:
                summary: Chat initialization error (chat.init.personal)
                value:
                  method: error
                  payload:
                    message: "User lookup failed"
                    event_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7:79000'
      responses:
        "200":
          description: Error accepted and processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Validation error (invalid format or missing required fields)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.completed.personal:
    post:
      summary: Outgoing message delivery confirmation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.completed.personal
                payload:
                  $ref: '#/components/schemas/MessageCompletedPayload'
            examples:
              message_completed:
                summary: Full delivery confirmation of outgoing message
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9001
                    external_message_id: ext_msg_555
              message_completed_partial:
                summary: >
                  Partial delivery confirmation of message with files:
                  some attachments delivered to client, some not
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9002
                    external_message_id: ext_msg_556
                    delivered_attachments:
                      - "555"
                    failed_attachments:
                      - "556"
      responses:
        "200":
          description: Event processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Validation error (invalid format or missing required fields)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Outgoing message with the specified inner_message_id not found in the scope of
            the webhook owner bot (cross-bot — attempt to confirm delivery of another bot's message).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.read.personal:
    post:
      summary: Message read by user (accepted as no-op)
      description: >
        Sent by the external application when a message is read by a user.
        Acloud accepts the event and returns 200 OK, but **full read tracking is not yet
        implemented** (no-op handler) — actual read semantics are future work. The contract
        is fixed so that the external application sending this hook does not receive 5xx.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.read.personal
                payload:
                  $ref: '#/components/schemas/MessageReadPayload'
            examples:
              message_read:
                summary: Message read
                value:
                  method: message.read.personal
                  payload:
                    message_id: msg_123
                    read_date: 1710752400
                    user_id: user_456
                    chat_id: chat_789
      responses:
        "200":
          description: Event accepted (no-op, no side effects)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "409":
          description: >
            Bot deactivated or account blocked.
            Stop sending events until you receive the outgoing hook bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Bot or account deleted.
            Remove the integration on your side and stop sending events.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error


components:
  schemas:

    IncomingEvent:
      type: object
      required:
        - method
        - payload
      properties:
        method:
          type: string
          enum:
            - message.new.personal
            - message.completed.personal
            - message.delete.personal
            - message.edit.personal
            - message.read.personal
            - error
          description: Event type
        payload:
          oneOf:
            - $ref: '#/components/schemas/MessageNewPayload'
            - $ref: '#/components/schemas/MessageCompletedPayload'
            - $ref: '#/components/schemas/MessageDeletedPayload'
            - $ref: '#/components/schemas/MessageEditedPayload'
            - $ref: '#/components/schemas/MessageReadPayload'
            - $ref: '#/components/schemas/ErrorPayload'
          discriminator:
            propertyName: method

    # ─── message.new.personal ─────────────────────────────────────────────────

    MessageNewPayload:
      type: object
      description: New incoming message from an external application user
      required:
        - external_message_id
        - external_user_id
        - external_chat_id
      properties:
        external_message_id:
          type: string
          description: Unique message ID in the external system
        external_user_id:
          type: string
          description: Unique user ID in the external system
        external_chat_id:
          type: string
          description: Unique chat ID in the external system
        text:
          type: string
          default: ""
          description: Message text
        send_date:
          type: integer
          description: Unix timestamp of message send time
          example: 1710752400
        user_data:
          $ref: '#/components/schemas/UserData'
        attachments:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/AttachmentObject'
          description: >
            Array of message attachments. If not specified or empty,
            the message consists of text only.
        direction:
          type: ["integer", "null"]
          enum: [0, 1, null]
          default: null
          description: >
            Message direction. 0 — inbound (from user to manager),
            1 — outbound (from manager to user, sent in the external system).
            If not specified — treated as inbound.
        has_failed_attachments:
          type: ["boolean", "null"]
          default: null
          description: >
            Optional flag declared by the external application that the original user message
            contained more attachments than were actually passed to Acloud in the attachments
            array. The reason can be anything on the external application side (size limit
            exceeded, download error, messenger policy, etc.). Currently Acloud accepts the
            field and logs the fact, but does not perform special processing and does not save
            it to the DB. The field is reserved for future implementation — symmetric with
            delivered_attachments / failed_attachments in the outgoing hook message.completed.personal.
        failed_attachments_count:
          type: ["integer", "null"]
          minimum: 0
          default: null
          description: >
            Optional count of files that the external application did not deliver.
            Only meaningful when has_failed_attachments=true. If sent without
            has_failed_attachments=true, the value is ignored and a warning is written
            to the Acloud log.

    # ─── message.delete.personal ──────────────────────────────────────────────

    MessageDeletedPayload:
      type: object
      description: Event of message deletion by user in the external system.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: Message ID in the external system
          example: msg_123
        deleted_date:
          type: integer
          description: Unix timestamp of message deletion time
          example: 1710752400

    # ─── message.read.personal ────────────────────────────────────────────────

    MessageReadPayload:
      type: object
      description: >
        Event of message read by user in the external system.
        Acloud accepts the payload, but in the current version **does not process** it
        (no-op handler). The contract is fixed for compatibility with future processing;
        all fields are optional (defaults in DTO).
      properties:
        message_id:
          type: string
          description: ID of the read message in the external system
          example: msg_123
          default: ""
        read_date:
          type: integer
          description: Unix timestamp of read time
          example: 1710752400
        user_id:
          type: string
          description: ID of the user who read the message
          example: user_456
          default: ""
        chat_id:
          type: string
          description: Chat ID
          example: chat_789
          default: ""

    # ─── message.edit.personal ────────────────────────────────────────────────

    MessageEditedPayload:
      type: object
      description: Event of message editing by user in the external system.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: Message ID in the external system
          example: msg_123
        edited_date:
          type: integer
          description: Unix timestamp of message edit time
          example: 1710752400
        new_text:
          type: string
          description: New message text after editing
          default: ""
          example: Corrected message text

    # ─── message.completed.personal ───────────────────────────────────────────

    MessageCompletedPayload:
      type: object
      description: >
        The external application confirms successful delivery of a message sent by our system.
        Used to link inner_message_id (ours) with external_message_id (theirs).


        Behavior with partial file delivery: if some files from the attachments array of the
        outgoing hook were delivered to the recipient and some were not, the external application
        still sends message.completed.personal and specifies the delivery result for each file
        in the delivered_attachments and failed_attachments fields.

        If all files were delivered — the fields can be omitted or the full list sent in
        delivered_attachments.

        If no files could be delivered (and the text part is also invalid for the client) —
        an error event is sent instead of message.completed.personal.
      required:
        - inner_message_id
        - external_message_id
      properties:
        inner_message_id:
          type: integer
          description: Message ID in our system (the inner_message_id value from the outgoing hook message.new.personal)
          example: 9001
        external_message_id:
          type: string
          description: ID assigned to the message in the external system
          example: ext_msg_555
        delivered_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            List of attachment IDs (corresponding to id from the attachments array of the outgoing
            message.new.personal) that were successfully delivered to the recipient. Optional field:
            can be omitted if all attachments were delivered.

            IMPORTANT: in the current version Acloud accepts this field but does not use it for
            special logic — the message status is set to OK (full delivery). The field is reserved
            for future partial delivery handling. The external application should send it now so it
            does not need to change its side when handling is added.
          example: ["555", "556"]
        failed_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            List of attachment IDs that could not be delivered to the recipient. Optional field:
            can be omitted if all attachments were delivered.

            IMPORTANT: in the current version Acloud accepts this field but does not use it for
            special logic — the message status is set to OK even with failed_attachments. The field
            is reserved for future partial delivery handling. The external application should send
            it now so it does not need to change its side when handling is added.
          example: ["557"]

    # ─── Shared types ─────────────────────────────────────────────────────────

    UserData:
      type: object
      description: External system user data
      properties:
        name:
          type: ["string", "null"]
          description: Display name
        username:
          type: ["string", "null"]
          description: Nickname
        phone:
          type: ["string", "null"]
        email:
          type: ["string", "null"]
        avatar_url:
          type: ["string", "null"]
          description: Avatar URL
        public_link:
          type: ["string", "null"]
          description: Public profile link

    AttachmentObject:
      type: object
      description: >
        Incoming message attachment metadata. Acloud will download the file from the provided
        URL and store it. Maximum allowed size — 50 MB (52428800 bytes); larger files are rejected.
      required:
        - id
        - url
        - filename
      properties:
        id:
          type: string
          description: >
            Unique attachment ID in the external system.
            Used for deduplication on repeated webhook calls.
        url:
          type: string
          description: URL from which Acloud downloads the file.
        filename:
          type: string
          description: Filename with extension.
        size:
          type: integer
          default: 0
          description: >
            File size in bytes. Must not exceed 50 MB (52428800).
            Acloud checks the size before and after download.
        type:
          type: string
          default: file
          description: >
            Attachment type hint (photo / file / video / audio).
            Acloud does not use this field — the actual type is determined by the mime type
            of the downloaded file.
        metadata:
          type: ["object", "null"]
          description: Arbitrary metadata (optional).

    # ─── error ────────────────────────────────────────────────────────────────

    ErrorPayload:
      type: object
      description: >
        Error event payload. The external application signals that it could not process
        an outgoing hook (message.new.personal or chat.init.personal).
      required:
        - event_id
      properties:
        message:
          type: string
          description: Human-readable error description
          example: "User not found"
        event_id:
          type: string
          description: >
            The value of the event_id field from the outgoing hook payload. Returned unchanged
            (as an opaque string). If the identifier is no longer known to Acloud (e.g., TTL has
            expired), the event is safely ignored without side effects.
          example: '550e8400-e29b-41d4-a716-446655440000:79000'

    # ─── Shared types ─────────────────────────────────────────────────────────

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string

    ConflictResponse:
      type: object
      description: >
        Bot deactivated or account blocked. The external application must stop sending events
        for this bot until it receives the outgoing hook bot.activated.
      required:
        - success
        - error
        - message
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Error code
          enum:
            - bot_deactivated
            - account_expired
          example: bot_deactivated
        message:
          type: string
          description: Human-readable reason description
          example: Bot is deactivated
