{
  "openapi": "3.0.3",
  "info": {
    "title": "MultiMensajes API",
    "version": "1.0.0",
    "description": "API REST de MultiMensajes. Permite enviar mensajes de WhatsApp, administrar celulares, configurar webhooks, gestionar saldo y mas.\n\n## Como autenticarse\nTodas las llamadas requieren un header `Authorization: Bearer <api_key>` donde `<api_key>` puede ser:\n- El formato moderno: `WA_<celular_id>_<signature>` (lo entrega el panel al crear el cliente)\n- El formato legacy: cualquier token mapeado en el celular como `wasenderapi_token` o `token_legacy`\n\n## URL base\n- Produccion: `https://api.multimensajes.com`\n- Sandbox (Plesk dev): `https://api.multimensajes.com` (mismo dominio, debug=true)",
    "contact": {
      "name": "Soporte MultiMensajes",
      "url": "https://panel.multimensajes.com"
    }
  },
  "servers": [
    {
      "url": "https://api.multimensajes.com",
      "description": "Produccion"
    }
  ],
  "tags": [
    {"name": "Mensajes", "description": "Envio de mensajes de WhatsApp"},
    {"name": "Clientes", "description": "Informacion de la cuenta"},
    {"name": "Celulares", "description": "Gestion de numeros de WhatsApp"},
    {"name": "Webhook", "description": "Configuracion del webhook OUT del cliente"},
    {"name": "Saldo", "description": "Consulta de saldo y consumo"},
    {"name": "Historial", "description": "Listado de mensajes enviados y recibidos"}
  ],
  "paths": {
    "/api/mensaje_enviar_texto.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un mensaje de texto",
        "description": "Encola un mensaje de texto. Si `directo=1`, lo envia al instante sin pasar por la cola.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/EnviarTextoRequest"}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mensaje encolado o enviado",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/MensajeResponse"}
              }
            }
          },
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "402": {"$ref": "#/components/responses/BadRequest"},
          "403": {"$ref": "#/components/responses/Forbidden"},
          "429": {"$ref": "#/components/responses/RateLimit"}
        }
      }
    },
    "/api/mensaje_enviar_imagen.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar una imagen (URL publica)",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarImagenRequest"},
        "responses": {
          "200": {"$ref": "#/components/responses/MensajeResponse"},
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/api/mensaje_enviar_imagen_base64.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar una imagen en base64",
        "description": "La API decodifica el base64 y lo guarda como archivo temporal. Se envia al instante y luego se borra.",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarArchivoB64Request"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_documento.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un documento (URL publica)",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarDocumentoRequest"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_documento_base64.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un documento en base64",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarArchivoB64Request"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_audio.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un audio (URL publica, ptt opcional)",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarAudioRequest"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_audio_base64.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un audio en base64",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarArchivoB64Request"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_video.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un video (URL publica)",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarVideoRequest"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_video_base64.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un video en base64",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarArchivoB64Request"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/mensaje_enviar_sticker.php": {
      "post": {
        "tags": ["Mensajes"],
        "summary": "Enviar un sticker (URL publica)",
        "requestBody": {"$ref": "#/components/requestBodies/EnviarStickerRequest"},
        "responses": {"200": {"$ref": "#/components/responses/MensajeResponse"}}
      }
    },
    "/api/info.php": {
      "get": {
        "tags": ["Clientes"],
        "summary": "Obtener informacion del cliente asociado a la API key",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/InfoResponse"}
              }
            }
          },
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/api/saldo.php": {
      "get": {
        "tags": ["Saldo"],
        "summary": "Obtener el saldo del cliente y del celular asociado",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/SaldoResponse"}
              }
            }
          },
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/api/celulares.php": {
      "get": {
        "tags": ["Celulares"],
        "summary": "Listar los celulares del cliente",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/CelularesListResponse"}
              }
            }
          },
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      }
    },
    "/api/celular.php": {
      "get": {
        "tags": ["Celulares"],
        "summary": "Obtener detalle de un celular",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "integer"}, "description": "ID del celular"}
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/CelularResponse"}
              }
            }
          },
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    },
    "/api/celular_crear.php": {
      "post": {
        "tags": ["Celulares"],
        "summary": "Crear un celular (sin vincular)",
        "description": "Crea el registro del celular. Para vincular con WhatsApp (QR) llamar despues a /api/celular_qr.php?id=N",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/CelularCrearRequest"}
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creado",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CelularResponse"}}}
          },
          "403": {"$ref": "#/components/responses/Forbidden"},
          "409": {"description": "Numero ya registrado"}
        }
      }
    },
    "/api/celular_editar.php": {
      "post": {
        "tags": ["Celulares"],
        "summary": "Editar campos de un celular",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "integer"}}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/CelularEditarRequest"}
            }
          }
        },
        "responses": {
          "200": {"$ref": "#/components/responses/Ok"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    },
    "/api/celular_eliminar.php": {
      "post": {
        "tags": ["Celulares"],
        "summary": "Eliminar un celular (hard delete con auditoria)",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "integer"}}
        ],
        "responses": {
          "200": {"description": "Eliminado"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    },
    "/api/celular_qr.php": {
      "post": {
        "tags": ["Celulares"],
        "summary": "Generar o recuperar el QR para vincular el WhatsApp",
        "description": "Si el celular no tiene session, crea una automaticamente. Devuelve el string del QR que debes mostrar al usuario (con una lib qrcode en JS) para que lo escanee desde el WhatsApp del celular.",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "integer"}}
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/QRResponse"}
              }
            }
          }
        }
      }
    },
    "/api/celular_status.php": {
      "get": {
        "tags": ["Celulares"],
        "summary": "Obtener el status actual de la sesion de WhatsApp",
        "parameters": [
          {"name": "id", "in": "query", "required": true, "schema": {"type": "integer"}}
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/StatusResponse"}
              }
            }
          }
        }
      }
    },
    "/api/webhook.php": {
      "get": {
        "tags": ["Webhook"],
        "summary": "Leer la configuracion actual del webhook",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/WebhookResponse"}
              }
            }
          }
        }
      },
      "post": {
        "tags": ["Webhook"],
        "summary": "Crear o actualizar el webhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/WebhookSetRequest"}
            }
          }
        },
        "responses": {"200": {"description": "OK"}}
      },
      "delete": {
        "tags": ["Webhook"],
        "summary": "Eliminar el webhook",
        "responses": {"200": {"description": "OK"}}
      }
    },
    "/api/webhook_configurar.php": {
      "post": {
        "tags": ["Webhook"],
        "summary": "[LEGACY] Configurar webhook (compatibilidad con api-legacy)",
        "description": "Endpoint antiguo, preferir /api/webhook.php. Acepta body con op=get para leer la config."
      }
    },
    "/api/historial_listar.php": {
      "get": {
        "tags": ["Historial"],
        "summary": "Listar mensajes enviados (historial con paginacion y filtros)",
        "parameters": [
          {"name": "celular_id", "in": "query", "schema": {"type": "integer"}},
          {"name": "estado", "in": "query", "schema": {"type": "string", "enum": ["enviado", "entregado", "leido", "error", "pendiente"]}},
          {"name": "desde", "in": "query", "schema": {"type": "string", "format": "date"}},
          {"name": "hasta", "in": "query", "schema": {"type": "string", "format": "date"}},
          {"name": "destinatario", "in": "query", "schema": {"type": "string"}},
          {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}
        ],
        "responses": {"200": {"description": "OK"}}
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key formato WA_<id>_<sig> o token legacy (wasenderapi_token / token_legacy)"
      }
    },
    "schemas": {
      "EnviarTextoRequest": {
        "type": "object",
        "required": ["key", "destinatario", "mensaje"],
        "properties": {
          "key": {"type": "string", "description": "API key (mismo valor que el Bearer header)"},
          "destinatario": {"type": "string", "description": "Numero con codigo de pais, sin +, ej: 5218711281619"},
          "mensaje": {"type": "string", "description": "Texto del mensaje (max 4096 chars)"},
          "directo": {"type": "integer", "enum": [0, 1], "default": 0, "description": "1 = enviar al instante sin cola"}
        }
      },
      "EnviarImagenRequest": {
        "type": "object",
        "required": ["key", "destinatario", "url"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "url": {"type": "string", "format": "uri", "description": "URL publica HTTPS de la imagen"},
          "caption": {"type": "string", "description": "Pie de foto opcional"}
        }
      },
      "EnviarDocumentoRequest": {
        "type": "object",
        "required": ["key", "destinatario", "url"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "url": {"type": "string", "format": "uri"},
          "filename": {"type": "string", "description": "Nombre con extension (.pdf, .doc, etc)"},
          "caption": {"type": "string"}
        }
      },
      "EnviarAudioRequest": {
        "type": "object",
        "required": ["key", "destinatario", "url"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "url": {"type": "string", "format": "uri"},
          "ptt": {"type": "boolean", "default": false, "description": "Push-to-talk (nota de voz)"},
          "caption": {"type": "string"}
        }
      },
      "EnviarVideoRequest": {
        "type": "object",
        "required": ["key", "destinatario", "url"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "url": {"type": "string", "format": "uri"},
          "caption": {"type": "string"}
        }
      },
      "EnviarStickerRequest": {
        "type": "object",
        "required": ["key", "destinatario", "url"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "url": {"type": "string", "format": "uri", "description": "URL de imagen PNG/WebP para sticker"}
        }
      },
      "EnviarArchivoB64Request": {
        "type": "object",
        "required": ["key", "destinatario", "data_base64", "mime", "filename"],
        "properties": {
          "key": {"type": "string"},
          "destinatario": {"type": "string"},
          "data_base64": {"type": "string", "description": "Contenido del archivo en base64 (sin prefijo data:...)"},
          "mime": {"type": "string", "description": "image/jpeg, image/png, application/pdf, audio/mp3, video/mp4, etc"},
          "filename": {"type": "string", "description": "Nombre con extension"},
          "caption": {"type": "string"}
        }
      },
      "MensajeResponse": {
        "type": "object",
        "properties": {
          "api_codigo": {"type": "integer"},
          "api_texto": {"type": "string", "example": "OK"},
          "api_descripcion": {"type": "string"},
          "msg_id": {"type": "string", "format": "uuid", "description": "ID del mensaje encolado/enviado"},
          "modo": {"type": "string", "enum": ["cola", "directo"]},
          "saldo": {"type": "integer", "description": "Saldo del celular DESPUES de la operacion"},
          "texto": {"type": "string", "description": "Mensaje para mostrar al usuario"},
          "desc": {"type": "string"}
        }
      },
      "InfoResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "cliente": {
            "type": "object",
            "properties": {
              "id": {"type": "integer"},
              "nombre": {"type": "string"},
              "email": {"type": "string"},
              "estado": {"type": "string", "enum": ["activo", "suspendido", "baja"]},
              "plan": {"type": "string", "enum": ["medido", "ilimitado"]},
              "max_celulares": {"type": "integer", "nullable": true},
              "consumo_mes_actual": {"type": "integer"},
              "created_at": {"type": "string"}
            }
          },
          "saldo_global": {"type": "integer"},
          "num_celulares": {"type": "integer"},
          "num_celulares_activos": {"type": "integer"},
          "mensajes_30d": {"type": "integer"}
        }
      },
      "SaldoResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "cliente_id": {"type": "integer"},
          "celular_id": {"type": "integer"},
          "saldo_global": {"type": "integer"},
          "saldo_celular": {"type": "integer"},
          "plan": {"type": "string"},
          "ilimitado": {"type": "boolean"},
          "consumo_mes": {"type": "integer"}
        }
      },
      "CelularesListResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "cliente_id": {"type": "integer"},
          "total": {"type": "integer"},
          "celulares": {
            "type": "array",
            "items": {"$ref": "#/components/schemas/Celular"}
          }
        }
      },
      "Celular": {
        "type": "object",
        "properties": {
          "id": {"type": "integer"},
          "numero": {"type": "string", "description": "Numero con codigo de pais"},
          "nombre_interno": {"type": "string", "nullable": true},
          "estado": {"type": "string", "enum": ["activo", "suspendido", "desconectado"]},
          "saldo_actual": {"type": "integer"},
          "max_envios_por_minuto": {"type": "integer"},
          "max_envios_por_hora": {"type": "integer"},
          "max_envios_por_dia": {"type": "integer", "nullable": true},
          "hora_inicio_permitido": {"type": "string", "example": "08:00"},
          "hora_fin_permitido": {"type": "string", "example": "22:00"},
          "wasenderapi_instancia": {"type": "string", "nullable": true},
          "tiene_wasenderapi_token": {"type": "boolean"},
          "tiene_token_legacy": {"type": "boolean"},
          "last_status_check": {"type": "string", "nullable": true},
          "last_status": {"type": "string", "nullable": true}
        }
      },
      "CelularResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "celular": {"$ref": "#/components/schemas/Celular"}
        }
      },
      "CelularCrearRequest": {
        "type": "object",
        "required": ["numero"],
        "properties": {
          "numero": {"type": "string", "description": "Numero con codigo de pais sin +, ej: 5218711281619"},
          "nombre_interno": {"type": "string", "description": "Etiqueta interna opcional"}
        }
      },
      "CelularEditarRequest": {
        "type": "object",
        "properties": {
          "estado": {"type": "string", "enum": ["activo", "suspendido", "desconectado"]},
          "nombre_interno": {"type": "string", "maxLength": 80},
          "max_envios_por_minuto": {"type": "integer", "minimum": 1, "maximum": 1000},
          "max_envios_por_hora": {"type": "integer", "minimum": 1, "maximum": 50000},
          "max_envios_por_dia": {"type": "integer", "minimum": 1, "maximum": 100000},
          "hora_inicio_permitido": {"type": "string", "pattern": "^\\d{2}:\\d{2}(:\\d{2})?$"},
          "hora_fin_permitido": {"type": "string", "pattern": "^\\d{2}:\\d{2}(:\\d{2})?$"}
        }
      },
      "QRResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "celular_id": {"type": "integer"},
          "wasenderapi_instancia": {"type": "string"},
          "qr": {"type": "string", "description": "String del QR (en formato '2@xxx,...') que debes convertir a imagen con una lib qrcode"},
          "status": {"type": "string", "enum": ["need_scan", "connected", "logged_out", "unknown"]}
        }
      },
      "StatusResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "celular_id": {"type": "integer"},
          "wasenderapi_instancia": {"type": "string"},
          "status": {"type": "string"},
          "status_actualizado_desde_wasenderapi": {"type": "boolean"},
          "ultimo_check": {"type": "string"}
        }
      },
      "WebhookResponse": {
        "type": "object",
        "properties": {
          "ok": {"type": "boolean"},
          "configurado": {"type": "boolean"},
          "cliente_id": {"type": "integer"},
          "url": {"type": "string"},
          "has_secret": {"type": "boolean"},
          "eventos": {"type": "array", "items": {"type": "string"}},
          "activo": {"type": "boolean"},
          "updated_at": {"type": "string"}
        }
      },
      "WebhookSetRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {"type": "string", "format": "uri", "pattern": "^https?://"},
          "secret": {"type": "string", "description": "Secret para firma HMAC-SHA256 (header X-MultiMensajes-Signature)"},
          "eventos": {"type": "array", "items": {"type": "string", "enum": ["messages.received", "message.status", "session.status", "all"]}},
          "activo": {"type": "boolean", "default": true}
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "api_codigo": {"type": "integer", "description": "Codigo de error"},
          "api_texto": {"type": "string", "description": "Mensaje corto"},
          "api_descripcion": {"type": "string", "description": "Detalle"},
          "saldo": {"type": "integer", "nullable": true}
        }
      }
    },
    "responses": {
      "Ok": {
        "description": "OK",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      },
      "Unauthorized": {
        "description": "API key invalida o faltante",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      },
      "BadRequest": {
        "description": "Parametros invalidos",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      },
      "Forbidden": {
        "description": "Operacion no permitida",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      },
      "NotFound": {
        "description": "Recurso no encontrado",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      },
      "RateLimit": {
        "description": "Rate limit excedido",
        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
      }
    }
  },
  "security": [{"BearerAuth": []}]
}
