openapi: 3.1.0
info:
  title: Arplyx API
  version: "1.0.0"
  description: >
    API pública de Arplyx para enviar mensajes y broadcasts de WhatsApp desde
    tus sistemas. Arplyx se encarga de la cola, los reintentos, el seguimiento
    de estados y el canal (Meta Official o WhatsApp Direct).


    **Autenticación:** todas las llamadas llevan tu API key en el header
    `x-api-key` (formato `ak_live_…`, sin prefijo `Bearer`). Se generan desde el
    portal (Portal → API Keys).


    **Idempotencia:** cada envío lleva un `externalId` único tuyo. Repetir el
    mismo `externalId` con el mismo payload devuelve el recurso original
    (`200`) sin reenviar; con un payload distinto devuelve `409`.
  contact:
    name: Arplyx
    email: info@arplyx.com
    url: https://docs.arplyx.com/
servers:
  - url: https://api.arplyx.com
    description: Producción
externalDocs:
  description: Documentación de la API
  url: https://docs.arplyx.com/api/
security:
  - ApiKeyAuth: []
tags:
  - name: Mensajes
    description: Envío y consulta de mensajes individuales y broadcasts.
  - name: Cuentas
    description: Cuentas de WhatsApp del cliente.
  - name: Sistema
    description: Estado del servicio.
paths:
  /messages:
    post:
      tags: [Mensajes]
      summary: Enviar un mensaje
      description: >
        Encola un mensaje de WhatsApp para un destinatario. La respuesta es
        inmediata; el envío real lo realiza el worker de Arplyx y el estado se
        sigue desde el portal.
      operationId: createMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMessageRequest"
            examples:
              texto:
                summary: Mensaje de texto (WhatsApp Direct)
                value:
                  externalId: pedido-10045
                  channel: whatsapp_direct
                  to: { type: phone, phoneE164: "+5491155551234" }
                  content: { type: text, text: "Hola! Tu pedido fue confirmado." }
                  whatsappAccountId: 0b8e2c1a-1111-2222-3333-444455556666
              plantilla:
                summary: Mensaje de plantilla (Meta Official)
                value:
                  externalId: pedido-10045
                  channel: whatsapp_meta
                  to: { type: phone, phoneE164: "+5491155551234" }
                  content:
                    type: template
                    template:
                      name: pedido_confirmado
                      language: es_AR
                      components: { body: ["Juan", "10045"] }
      responses:
        "201":
          description: Mensaje creado y encolado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageResponse" }
        "200":
          description: Repetición idempotente (mismo externalId y payload). Devuelve el mensaje original.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageResponse" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403":
          description: La función no está disponible en tu plan (p. ej. plantillas en Free).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: templates_not_available, plan: free, message: "El envío de plantillas requiere un plan pago." }
        "409": { $ref: "#/components/responses/Conflict" }
        "422":
          description: La cuenta de WhatsApp es inválida (no existe, no es tuya o el tipo no coincide con el canal).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: invalid_whatsapp_account, message: "whatsappAccountId no válido para el canal." }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/InternalError" }
  /messages/broadcast:
    post:
      tags: [Mensajes]
      summary: Enviar un broadcast a una lista
      description: >
        Envía un mensaje de texto a todos los contactos de una lista creada en
        el portal. Genera un mensaje individual por contacto (cada uno descuenta
        de tu cuota mensual). Requiere plan Basic o Pro.
      operationId: createBroadcast
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateBroadcastRequest" }
            examples:
              texto:
                value:
                  externalId: promo-junio-2026
                  listId: 3f1d4e22-1111-2222-3333-444455556666
                  channel: whatsapp_direct
                  content: { type: text, text: "Hola! Este mes 2x1 en turnos." }
                  whatsappAccountId: 0b8e2c1a-1111-2222-3333-444455556666
      responses:
        "201":
          description: Broadcast creado y encolado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BroadcastResponse" }
        "200":
          description: Repetición idempotente. Devuelve el broadcast original.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BroadcastResponse" }
        "400":
          description: >
            Error de validación o error propio de broadcast (`empty_list`,
            `broadcast_too_large`, `whatsapp_account_invalid`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403":
          description: Tu plan no incluye broadcasts.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: feature_not_available }
        "404":
          description: La lista no existe o no es de tu cuenta.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: list_not_found }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/InternalError" }
  /messages/{id}:
    get:
      tags: [Mensajes]
      summary: Consultar el estado de un mensaje
      description: >
        Devuelve el estado y los datos de un mensaje del cliente autenticado.
        `id` puede ser el `messageId` que devolvió `POST /messages` o tu propio
        `externalId`.
      operationId: getMessage
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: messageId de Arplyx (UUID) o tu externalId.
      responses:
        "200":
          description: Estado del mensaje.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: No existe un mensaje con ese id para tu cuenta.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: message_not_found }
        "500": { $ref: "#/components/responses/InternalError" }
  /whatsapp-accounts:
    get:
      tags: [Cuentas]
      summary: Listar cuentas de WhatsApp
      description: >
        Lista las cuentas de WhatsApp del cliente autenticado, con su tipo,
        estado y el `id` que se usa como `whatsappAccountId` al enviar.
      operationId: listWhatsappAccounts
      responses:
        "200":
          description: Cuentas de WhatsApp del cliente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  accounts:
                    type: array
                    items: { $ref: "#/components/schemas/WhatsAppAccount" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "500": { $ref: "#/components/responses/InternalError" }
  /health:
    get:
      tags: [Sistema]
      summary: Health check
      operationId: health
      security: []
      responses:
        "200":
          description: Servicio operativo.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  timestamp: { type: string, format: date-time }
                  service: { type: string, example: arplyx-gateway }
                  version: { type: string, example: "0.1.0" }
                  environment: { type: string, example: production }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key de Arplyx (formato `ak_live_…`). Se genera en Portal → API Keys.
  schemas:
    PhoneRecipient:
      type: object
      required: [type, phoneE164]
      properties:
        type:
          type: string
          enum: [phone]
        phoneE164:
          type: string
          description: Número en formato E.164 (`+` seguido de 7 a 15 dígitos). También se acepta número nacional, resuelto contra el país por defecto.
          example: "+5491155551234"
    TextContent:
      type: object
      required: [type, text]
      properties:
        type: { type: string, enum: [text] }
        text:
          type: string
          minLength: 1
          maxLength: 10000
          example: "Hola! Tu pedido fue confirmado."
    TemplateContent:
      type: object
      required: [type, template]
      properties:
        type: { type: string, enum: [template] }
        template:
          type: object
          required: [name, language]
          properties:
            name: { type: string, minLength: 1, maxLength: 200, example: pedido_confirmado }
            language: { type: string, minLength: 1, maxLength: 20, example: es_AR }
            components:
              type: object
              properties:
                body: { type: array, items: { type: string } }
                header: { type: array, items: { type: string } }
                buttons: { type: array, items: { type: string } }
    CreateMessageRequest:
      type: object
      required: [externalId, to, content]
      properties:
        externalId:
          type: string
          minLength: 1
          maxLength: 200
          description: Tu identificador único. Base de la idempotencia.
          example: pedido-10045
        channel:
          type: string
          enum: [whatsapp_meta, whatsapp_direct]
          default: whatsapp_meta
        to:
          $ref: "#/components/schemas/PhoneRecipient"
        content:
          oneOf:
            - $ref: "#/components/schemas/TextContent"
            - $ref: "#/components/schemas/TemplateContent"
          discriminator:
            propertyName: type
        whatsappAccountId:
          type: string
          format: uuid
          description: Obligatorio para `whatsapp_direct`. Para `whatsapp_meta`, solo si tenés más de una cuenta Meta conectada.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: Datos arbitrarios tuyos; se guardan junto al mensaje.
        ttlSeconds:
          type: integer
          minimum: 1
          description: Vigencia en cola. Default 604800 (7 días), máximo 2592000 (30 días); se recorta por la retención del plan.
    CreateBroadcastRequest:
      type: object
      required: [externalId, listId, channel, content]
      properties:
        externalId: { type: string, minLength: 1, maxLength: 200, example: promo-junio-2026 }
        listId:
          type: string
          format: uuid
          description: ID de la lista de contactos (sección Listas del portal). Debe ser tuya.
        channel:
          type: string
          enum: [whatsapp_meta, whatsapp_direct]
        whatsappAccountId:
          type: string
          format: uuid
          description: Obligatorio para `whatsapp_direct` (cuenta conectada). Opcional para `whatsapp_meta`.
        content:
          $ref: "#/components/schemas/TextContent"
        ttlSeconds:
          type: integer
          minimum: 1
    MessageResponse:
      type: object
      properties:
        messageId: { type: string, format: uuid }
        externalId: { type: string }
        status: { type: string, example: pending }
        queued: { type: boolean, example: true }
    BroadcastResponse:
      type: object
      properties:
        broadcastId: { type: string, format: uuid }
        externalId: { type: string }
        listId: { type: string, format: uuid }
        messageCount: { type: integer, example: 482 }
        status: { type: string, example: queued }
    MessageStatus:
      type: object
      properties:
        messageId: { type: string, format: uuid }
        externalId: { type: string }
        recipient: { type: string, example: "+5491155551234" }
        channel: { type: string, enum: [whatsapp_meta, whatsapp_direct] }
        status:
          type: string
          enum: [pending, queued, sent, delivered, read, failed]
        attemptCount: { type: integer, example: 1 }
        lastErrorCode: { type: string, nullable: true }
        broadcastId: { type: string, format: uuid, nullable: true }
        createdAt: { type: string, format: date-time }
        sentAt: { type: string, format: date-time, nullable: true }
        deliveredAt: { type: string, format: date-time, nullable: true }
        failedAt: { type: string, format: date-time, nullable: true }
    WhatsAppAccount:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Se usa como whatsappAccountId al enviar.
        type: { type: string, example: WHATSAPP_DIRECT }
        status: { type: string, example: CONNECTED }
        phoneNumber: { type: string, nullable: true, example: "+5491155551234" }
        displayName: { type: string, nullable: true }
        connectedAt: { type: string, format: date-time, nullable: true }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        message: { type: string }
    ValidationErrorBody:
      type: object
      properties:
        error: { type: string, example: validation_error }
        details:
          type: array
          items:
            type: object
            properties:
              path: { type: string, example: to.phoneE164 }
              message: { type: string, example: Invalid E.164 phone number }
  responses:
    ValidationError:
      description: El payload no pasa la validación. Incluye `details` por campo.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ValidationErrorBody" }
    Unauthorized:
      description: Falta la API key, es inválida o fue revocada.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Invalid API key }
    Conflict:
      description: El `externalId` ya existe con un payload distinto.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: conflict }
    QuotaExceeded:
      description: Cuota mensual agotada. Incluye `limit`, `used` y `resetsAt`.
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string, example: quota_exceeded }
              limit: { type: integer, example: 500 }
              used: { type: integer, example: 500 }
              resetsAt: { type: string, format: date-time }
    InternalError:
      description: Error inesperado de Arplyx. Reintentá con el mismo `externalId` (es seguro por idempotencia).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Internal server error }
