openapi: 3.1.0
info:
  title: Nahui Connect API
  version: 1.0.0
  description: >-
    API pública para responder conversaciones de WhatsApp recibidas mediante
    Nahui Connect. Los destinatarios se identifican exclusivamente por
    conversationId; no se aceptan teléfonos ni destinatarios arbitrarios.
  contact:
    name: Nahui Studio
    url: https://nahui.studio
  license:
    name: Software y API propietarios de Nahui Studio
    identifier: LicenseRef-Nahui-Proprietary

servers:
  - url: https://api.nahui.studio
    description: Producción

tags:
  - name: Mensajes
    description: Respuestas de texto a conversaciones existentes.
  - name: Plantillas
    description: Plantillas de WhatsApp aprobadas para conversaciones existentes.

security:
  - connectionApiKey: []

paths:
  /v1/connect/relay/{connectionId}/reply:
    post:
      operationId: replyToConnectConversation
      tags: [Mensajes]
      summary: Responder con texto a una conversación existente
      description: >-
        Envía texto a un conversationId emitido previamente por el webhook de
        la misma conexión. No acepta campos de destinatario.
      parameters:
        - $ref: '#/components/parameters/ConnectionId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplyRequest'
            examples:
              simple:
                value:
                  conversationId: ncv_8354e263-3958-4602-b54c-6b6fd84720c2
                  message:
                    text: Tu cita quedó confirmada.
      responses:
        '200':
          description: Mensaje enviado o solicitud duplicada ya aceptada.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RelaySuccess'
                  - $ref: '#/components/schemas/DuplicateSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/BalanceExhausted'
        '404':
          $ref: '#/components/responses/ConversationNotFound'
        '409':
          $ref: '#/components/responses/WhatsappNotConfigured'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/UpstreamFailure'

  /v1/connect/relay/{connectionId}/template:
    post:
      operationId: sendConnectTemplate
      tags: [Plantillas]
      summary: Enviar una plantilla de WhatsApp aprobada
      description: >-
        Envía una plantilla existente en Meta a una conversación que nació de
        un mensaje entrante. Límite de 5 plantillas por minuto por conexión.
      parameters:
        - $ref: '#/components/parameters/ConnectionId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateRequest'
            examples:
              simple:
                value:
                  conversationId: ncv_8354e263-3958-4602-b54c-6b6fd84720c2
                  templateName: recordatorio_cita
                  language: es_MX
              withBodyParameters:
                value:
                  conversationId: ncv_8354e263-3958-4602-b54c-6b6fd84720c2
                  templateName: recordatorio_cita
                  language: es_MX
                  components:
                    - type: body
                      parameters:
                        - type: text
                          text: Daniel
                        - type: text
                          text: 26 de agosto a las 3:00 p.m.
      responses:
        '200':
          description: Plantilla enviada o solicitud duplicada ya aceptada.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RelaySuccess'
                  - $ref: '#/components/schemas/DuplicateSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/BalanceExhausted'
        '404':
          $ref: '#/components/responses/ConversationNotFound'
        '409':
          $ref: '#/components/responses/WhatsappNotConfigured'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/UpstreamFailure'

webhooks:
  messageReceived:
    post:
      operationId: receiveNahuiConnectMessage
      summary: Evento enviado por Nahui al webhook configurado
      security: []
      description: >-
        Verifica X-Nahui-Signature-256 sobre el cuerpo crudo antes de parsear.
        El servidor debe responder antes de 15 segundos.
      parameters:
        - name: X-Nahui-Event
          in: header
          required: true
          schema:
            type: string
            const: message.received
        - name: X-Nahui-Delivery
          in: header
          required: true
          schema:
            type: string
            pattern: '^ncd_.+'
        - name: X-Nahui-Signature-256
          in: header
          required: true
          schema:
            type: string
            pattern: '^sha256=[A-Za-z0-9_-]+$'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageReceivedEvent'
      responses:
        '200':
          description: Evento aceptado, opcionalmente con respuesta inmediata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '401':
          description: Firma ausente o inválida.

components:
  securitySchemes:
    connectionApiKey:
      type: http
      scheme: bearer
      bearerFormat: nhc_...
      description: API key de una sola conexión, generada en Nahui Connect.

  parameters:
    ConnectionId:
      name: connectionId
      in: path
      required: true
      schema:
        type: string
        pattern: '^ncc_.+'
      description: ID de la conexión propietaria de la API key.

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 200
        pattern: '^[A-Za-z0-9._:-]{8,200}$'
      description: >-
        Identificador único del envío lógico. Reutilizar únicamente al
        reintentar el mismo envío.

  schemas:
    ReplyRequest:
      type: object
      additionalProperties: false
      required: [conversationId, message]
      properties:
        conversationId:
          $ref: '#/components/schemas/ConversationId'
        message:
          type: object
          additionalProperties: false
          required: [text]
          properties:
            text:
              type: string
              minLength: 1
              maxLength: 4000

    TemplateRequest:
      type: object
      additionalProperties: false
      required: [conversationId, templateName]
      properties:
        conversationId:
          $ref: '#/components/schemas/ConversationId'
        templateName:
          type: string
          minLength: 3
          maxLength: 512
          pattern: '^[a-z0-9_]{3,512}$'
        language:
          type: string
          minLength: 1
          maxLength: 30
          default: es_MX
        components:
          type: array
          maxItems: 20
          items:
            type: object
            additionalProperties: true

    ConversationId:
      type: string
      pattern: '^ncv_.+'
      maxLength: 160

    Delivery:
      type: object
      additionalProperties: false
      required: [externalMessageId, status]
      properties:
        externalMessageId:
          type: [string, 'null']
        status:
          type: string
          const: sent

    RelaySuccess:
      type: object
      additionalProperties: false
      required: [success, conversationId, delivery]
      properties:
        success:
          type: boolean
          const: true
        conversationId:
          $ref: '#/components/schemas/ConversationId'
        delivery:
          $ref: '#/components/schemas/Delivery'

    DuplicateSuccess:
      type: object
      additionalProperties: false
      required: [success, duplicate]
      properties:
        success:
          type: boolean
          const: true
        duplicate:
          type: boolean
          const: true

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string

    WebhookResponse:
      type: object
      additionalProperties: false
      properties:
        reply:
          type: object
          additionalProperties: false
          required: [text]
          properties:
            text:
              type: string
              minLength: 1
              maxLength: 4000

    MessageReceivedEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - type
        - deliveryId
        - projectId
        - connectionId
        - conversationId
        - channel
        - contact
        - message
        - receivedAt
      properties:
        id:
          type: string
          pattern: '^nce_.+'
        type:
          type: string
          const: message.received
        deliveryId:
          type: string
          pattern: '^ncd_.+'
        projectId:
          type: string
          pattern: '^ncp_.+'
        connectionId:
          type: string
          pattern: '^ncc_.+'
        conversationId:
          $ref: '#/components/schemas/ConversationId'
        channel:
          type: string
          const: whatsapp
        contact:
          type: object
          additionalProperties: false
          required: [id, displayName]
          properties:
            id:
              type: string
            displayName:
              type: string
        message:
          type: object
          required: [id, type, timestamp]
          properties:
            id:
              type: string
            type:
              type: string
            timestamp:
              type: string
              format: date-time
            text:
              type: string
              maxLength: 4000
            button:
              type: string
            interactive:
              type: object
            media:
              type: object
        receivedAt:
          type: string
          format: date-time

  responses:
    BadRequest:
      description: Solicitud inválida o destinatario prohibido.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Unauthorized:
      description: Credencial ausente o inválida.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    BalanceExhausted:
      description: Sin mensajes incluidos ni saldo de recarga.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: No quedan mensajes incluidos ni saldo de recargas.
            code: CONNECT_MESSAGE_BALANCE_EXHAUSTED

    ConversationNotFound:
      description: Conversación inexistente, bloqueada o no elegible.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    WhatsappNotConfigured:
      description: La conexión de WhatsApp no está configurada correctamente.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: La conexión de WhatsApp no está configurada.
            code: CONNECT_WHATSAPP_NOT_CONFIGURED

    PayloadTooLarge:
      description: Cuerpo superior al límite permitido.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    RateLimited:
      description: Límite temporal alcanzado. Respetar Retry-After cuando exista.
      headers:
        Retry-After:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    UpstreamFailure:
      description: Meta no pudo completar el envío.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'