# WAzion - Guia Completa de Integraciones Avanzadas

> **Version:** 1.1
> **Ultima actualizacion:** Enero 2026
> **Destinatarios:** Desarrolladores que deseen integrar sus sistemas con WAzion

---

## Tabla de Contenidos

1. [Introduccion](#introduccion)
2. [Funciones Personalizadas IA](#funciones-personalizadas-ia)
   - [Que son las Funciones Personalizadas](#que-son-las-funciones-personalizadas)
   - [Estructura JSON Completa](#estructura-json-completa)
   - [Tipos de Autenticacion](#tipos-de-autenticacion)
   - [Ejemplos Practicos](#ejemplos-practicos-funciones)
   - [Implementacion del Endpoint](#implementacion-del-endpoint-funciones)
3. [CRM Endpoints Personalizados](#crm-endpoints-personalizados)
   - [Que son los CRM Endpoints](#que-son-los-crm-endpoints)
   - [Tipos de Endpoints Disponibles](#tipos-de-endpoints-disponibles)
   - [Estructura JSON de Configuracion](#estructura-json-de-configuracion-crm)
   - [Implementacion de cada Endpoint](#implementacion-de-cada-endpoint)
4. [Alertas Automaticas (Webhooks)](#alertas-automaticas-webhooks)
   - [Como funcionan las Alertas](#como-funcionan-las-alertas)
   - [Eventos Disponibles](#eventos-disponibles)
   - [Formato del Payload](#formato-del-payload)
   - [Verificacion de Firma HMAC](#verificacion-de-firma-hmac)
   - [Implementacion del Receptor](#implementacion-del-receptor)
5. [Codigos de Ejemplo Completos](#codigos-de-ejemplo-completos)
6. [API de Estado de Mensajes WhatsApp](#api-de-estado-de-mensajes-whatsapp-message-status)
7. [Errores Comunes y Solucion](#errores-comunes-y-solucion)
8. [Checklist de Implementacion](#checklist-de-implementacion)

---

## Introduccion

WAzion permite tres tipos de integraciones avanzadas que conectan tu sistema CRM/ERP con la plataforma:

| Tipo | Descripcion | Direccion del Flujo |
|------|-------------|---------------------|
| **Funciones Personalizadas IA** | La IA de WAzion llama a tus APIs para obtener informacion en tiempo real | WAzion -> Tu Sistema |
| **CRM Endpoints** | WAzion consulta tu sistema para mostrar datos de clientes en el panel | WAzion -> Tu Sistema |
| **Alertas Automaticas** | WAzion te notifica cuando ocurren eventos importantes | WAzion -> Tu Sistema |

---

## Funciones Personalizadas IA

### Que son las Funciones Personalizadas

Las Funciones Personalizadas permiten que la IA de WAzion llame a APIs externas de tu negocio durante una conversacion con un cliente. Esto permite:

- Consultar disponibilidad de productos/servicios en tiempo real
- Realizar reservas automaticas
- Verificar estado de pedidos
- Consultar precios dinamicos
- Cualquier operacion que tu API pueda realizar

**Flujo de ejecucion:**
```
Cliente pregunta -> IA detecta necesidad -> IA llama tu API -> Tu API responde -> IA usa la info para responder
```

### Estructura JSON Completa

El JSON de configuracion es un **array de objetos**, donde cada objeto define una funcion:

```json
[
  {
    "name": "nombreDeLaFuncion",
    "description": "Descripcion clara de que hace esta funcion. La IA usa esto para decidir cuando llamarla.",
    "parameters": {
      "type": "object",
      "properties": {
        "parametro1": {
          "type": "string",
          "description": "Descripcion del parametro para que la IA sepa que valor pasar"
        },
        "parametro2": {
          "type": "number",
          "description": "Otro parametro numerico"
        },
        "endpoint": {
          "url": "https://tu-api.com/endpoint",
          "method": "POST",
          "format": "json",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer TU_TOKEN_SECRETO"
            }
          }
        }
      },
      "required": ["parametro1"]
    }
  }
]
```

### Campos Obligatorios

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `name` | string | Nombre unico de la funcion (sin espacios, usar camelCase o snake_case) |
| `description` | string | Descripcion clara para que la IA entienda cuando usar esta funcion |
| `parameters.type` | string | Siempre "object" |
| `parameters.properties` | object | Define los parametros que recibira tu API |
| `parameters.properties.endpoint` | object | **OBLIGATORIO** - Configuracion de tu endpoint |
| `parameters.properties.endpoint.url` | string | URL completa de tu API (debe ser HTTPS en produccion) |
| `parameters.properties.endpoint.method` | string | GET, POST, PUT, DELETE o PATCH |

### Campos Opcionales

| Campo | Tipo | Default | Descripcion |
|-------|------|---------|-------------|
| `parameters.properties.endpoint.format` | string | "json" | "json" o "form" (application/x-www-form-urlencoded) |
| `parameters.properties.endpoint.auth` | object | null | Configuracion de autenticacion |
| `parameters.required` | array | [] | Lista de parametros obligatorios |

### Especificaciones Tecnicas de Ejecucion

#### Timeouts
| Parametro | Valor | Descripcion |
|-----------|-------|-------------|
| `CURLOPT_TIMEOUT` | 240 segundos | Tiempo maximo total de la peticion |
| `CURLOPT_CONNECTTIMEOUT` | 10 segundos | Tiempo maximo para establecer conexion |
| `CURLOPT_FOLLOWLOCATION` | true | Sigue redirecciones automaticamente |
| `CURLOPT_MAXREDIRS` | 5 | Maximo de redirecciones permitidas |

#### Reintentos Automaticos con Exponential Backoff

WAzion reintenta automaticamente las peticiones fallidas usando exponential backoff:

| Intento | Espera base | Con jitter | Descripcion |
|---------|-------------|------------|-------------|
| 1 | 0s | 0s | Inmediato |
| 2 | 1s | ~1.0-1.5s | Primera retry |
| 3 | 2s | ~2.0-2.5s | Segunda retry |
| 4 | 4s | ~4.0-4.5s | Tercera retry (maximo) |

**Maximo 3 reintentos** (4 intentos totales). El jitter es aleatorio entre 0-500ms para evitar thundering herd.

**Formula del backoff:** `espera = min(baseSeconds * 2^(intento-2), 16) + random(0, 0.5)`

#### Codigos HTTP que Triggerean Reintentos

| Codigo | Descripcion |
|--------|-------------|
| 429 | Too Many Requests (rate limit) |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
| 504 | Gateway Timeout |

Tambien se reintenta en errores de conexion (timeout, DNS, etc.).

#### Codigos HTTP que NO Reintentan

| Codigo | Descripcion |
|--------|-------------|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 4xx (otros) | Error del cliente |

Estos errores se consideran permanentes y no se reintentan.

#### Parametro Reservado: phone

Si tu funcion declara un parametro llamado `phone`, WAzion lo sobreescribira automaticamente con el telefono del cliente actual en formato E.164 (ej: `+34612345678`). Esto garantiza que siempre se use el telefono correcto del cliente con quien se esta conversando, independientemente del valor que envie la IA.

Ejemplo de uso:

```json
{
  "phone": {
    "type": "string",
    "description": "Customer phone number"
  }
}
```

Tu endpoint recibira siempre el telefono real del cliente, no el que la IA decida enviar. Esto es util para enviar notificaciones, registrar consultas o personalizar respuestas segun el cliente.

> **Nota:** El nombre `phone` esta reservado para esta inyeccion automatica. Si necesitas un parametro de telefono que NO sea el del cliente (ej: un telefono de destino diferente), usa otro nombre como `destination_phone` o `target_number`.

### Tipos de Autenticacion

#### 1. Header Authentication (Recomendado)

Los campos de auth se envian como headers HTTP:

```json
{
  "auth": {
    "type": "header",
    "fields": {
      "Authorization": "Bearer sk_live_abc123xyz",
      "X-API-Key": "mi_api_key"
    }
  }
}
```

**Request resultante:**
```http
POST /api/reservar HTTP/1.1
Host: tu-api.com
Authorization: Bearer sk_live_abc123xyz
X-API-Key: mi_api_key
Content-Type: application/json

{"fecha": "2025-01-15", "hora": "14:00"}
```

#### 2. Query Authentication

Los campos de auth se agregan como parametros en la URL:

```json
{
  "auth": {
    "type": "query",
    "fields": {
      "api_key": "mi_api_key",
      "secret": "mi_secreto"
    }
  }
}
```

**Request resultante:**
```http
POST /api/reservar?api_key=mi_api_key&secret=mi_secreto HTTP/1.1
Host: tu-api.com
Content-Type: application/json

{"fecha": "2025-01-15", "hora": "14:00"}
```

#### 3. Basic Authentication

Autenticacion HTTP Basic estandar:

```json
{
  "auth": {
    "type": "basic",
    "fields": {
      "username": "mi_usuario",
      "password": "mi_contrasenya"
    }
  }
}
```

**Request resultante:**
```http
POST /api/reservar HTTP/1.1
Host: tu-api.com
Authorization: Basic bWlfdXN1YXJpbzptaV9jb250cmFzZW55YQ==
Content-Type: application/json

{"fecha": "2025-01-15", "hora": "14:00"}
```

#### 4. Body Authentication

Los campos de auth se incluyen en el body JSON junto con los parametros:

```json
{
  "auth": {
    "type": "body",
    "fields": {
      "api_token": "mi_token_secreto",
      "shop_id": "12345"
    }
  }
}
```

**Request resultante:**
```http
POST /api/reservar HTTP/1.1
Host: tu-api.com
Content-Type: application/json

{
  "api_token": "mi_token_secreto",
  "shop_id": "12345",
  "fecha": "2025-01-15",
  "hora": "14:00"
}
```

### Ejemplos Practicos Funciones

#### Ejemplo 1: Consultar Disponibilidad de Reservas

```json
[
  {
    "name": "consultarDisponibilidad",
    "description": "Consulta los horarios disponibles para reservar una mesa o habitacion en una fecha especifica",
    "parameters": {
      "type": "object",
      "properties": {
        "fecha": {
          "type": "string",
          "description": "Fecha de la reserva en formato YYYY-MM-DD"
        },
        "num_personas": {
          "type": "number",
          "description": "Numero de personas para la reserva"
        },
        "tipo_servicio": {
          "type": "string",
          "description": "Tipo de servicio: 'almuerzo', 'cena', 'habitacion'"
        },
        "endpoint": {
          "url": "https://mirestaurante.com/api/v1/disponibilidad",
          "method": "GET",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer api_key_restaurante_123"
            }
          }
        }
      },
      "required": ["fecha", "num_personas"]
    }
  }
]
```

**Tu endpoint debe responder:**
```json
{
  "disponible": true,
  "horarios": ["13:00", "13:30", "14:00", "20:00", "20:30", "21:00"],
  "mensaje": "Availability for 4 people on January 15"
}
```

#### Ejemplo 2: Realizar Reserva

```json
[
  {
    "name": "realizarReserva",
    "description": "Crea una nueva reserva para un cliente. Usa esta funcion cuando el cliente confirme que quiere reservar.",
    "parameters": {
      "type": "object",
      "properties": {
        "nombre_cliente": {
          "type": "string",
          "description": "Nombre completo del cliente"
        },
        "telefono": {
          "type": "string",
          "description": "Telefono de contacto del cliente"
        },
        "fecha": {
          "type": "string",
          "description": "Fecha de la reserva en formato YYYY-MM-DD"
        },
        "hora": {
          "type": "string",
          "description": "Hora de la reserva en formato HH:MM"
        },
        "num_personas": {
          "type": "number",
          "description": "Numero de personas"
        },
        "notas": {
          "type": "string",
          "description": "Notas especiales del cliente (alergias, preferencias, etc)"
        },
        "endpoint": {
          "url": "https://mirestaurante.com/api/v1/reservas",
          "method": "POST",
          "format": "json",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer api_key_restaurante_123"
            }
          }
        }
      },
      "required": ["nombre_cliente", "telefono", "fecha", "hora", "num_personas"]
    }
  }
]
```

**Tu endpoint debe responder:**
```json
{
  "success": true,
  "reserva_id": "RES-2025-00123",
  "mensaje": "Reservation confirmed for Juan Garcia, 4 people, January 15 at 20:00",
  "codigo_confirmacion": "ABC123"
}
```

#### Ejemplo 3: Consultar Estado de Pedido

```json
[
  {
    "name": "consultarPedido",
    "description": "Consulta el estado actual de un pedido usando su numero de referencia",
    "parameters": {
      "type": "object",
      "properties": {
        "numero_pedido": {
          "type": "string",
          "description": "Numero de referencia del pedido"
        },
        "endpoint": {
          "url": "https://mitienda.com/api/pedidos/estado",
          "method": "GET",
          "auth": {
            "type": "query",
            "fields": {
              "api_key": "tienda_api_key_xyz"
            }
          }
        }
      },
      "required": ["numero_pedido"]
    }
  }
]
```

#### Ejemplo 4: Buscar Productos con Stock

```json
[
  {
    "name": "buscarProducto",
    "description": "Busca productos en el catalogo y devuelve informacion de precio y disponibilidad",
    "parameters": {
      "type": "object",
      "properties": {
        "termino_busqueda": {
          "type": "string",
          "description": "Nombre o descripcion del producto a buscar"
        },
        "categoria": {
          "type": "string",
          "description": "Categoria de productos (opcional)"
        },
        "solo_disponibles": {
          "type": "boolean",
          "description": "Si es true, solo devuelve productos con stock"
        },
        "endpoint": {
          "url": "https://mitienda.com/api/productos/buscar",
          "method": "POST",
          "format": "json",
          "auth": {
            "type": "body",
            "fields": {
              "store_token": "token_tienda_123"
            }
          }
        }
      },
      "required": ["termino_busqueda"]
    }
  }
]
```

### Implementacion del Endpoint Funciones

#### PHP - Ejemplo Completo

```php
<?php
/**
 * API de Disponibilidad para WAzion
 * Archivo: /api/v1/disponibilidad.php
 */

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Manejo de preflight CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Verificar autenticacion
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$expectedToken = 'Bearer api_key_restaurante_123';

if ($authHeader !== $expectedToken) {
    http_response_code(401);
    echo json_encode([
        'success' => false,
        'error' => 'Invalid authentication token'
    ]);
    exit;
}

// Get parameters (GET or POST)
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $fecha = $_GET['fecha'] ?? null;
    $numPersonas = (int)($_GET['num_personas'] ?? 0);
    $tipoServicio = $_GET['tipo_servicio'] ?? 'cena';
} else {
    $input = json_decode(file_get_contents('php://input'), true);
    $fecha = $input['fecha'] ?? null;
    $numPersonas = (int)($input['num_personas'] ?? 0);
    $tipoServicio = $input['tipo_servicio'] ?? 'cena';
}

// Validations
if (!$fecha) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'error' => 'The fecha parameter is required'
    ]);
    exit;
}

if ($numPersonas < 1 || $numPersonas > 20) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'error' => 'Number of people must be between 1 and 20'
    ]);
    exit;
}

// YOUR BUSINESS LOGIC: Query real availability from your database
// This is a simplified example
try {
    // Here you would connect to your database and query real availability
    // $disponibilidad = consultarDisponibilidadEnBD($fecha, $numPersonas, $tipoServicio);

    // Ejemplo de respuesta simulada:
    $horariosDisponibles = [];
    $baseHoras = $tipoServicio === 'almuerzo'
        ? ['13:00', '13:30', '14:00', '14:30']
        : ['20:00', '20:30', '21:00', '21:30'];

    // Simulate some time slots being taken
    foreach ($baseHoras as $hora) {
        if (rand(0, 1) === 1) { // In production, query real DB
            $horariosDisponibles[] = $hora;
        }
    }

    if (empty($horariosDisponibles)) {
        echo json_encode([
            'disponible' => false,
            'horarios' => [],
            'mensaje' => "Sorry, no availability for $numPersonas people on $fecha. We recommend trying another date."
        ]);
    } else {
        echo json_encode([
            'disponible' => true,
            'horarios' => $horariosDisponibles,
            'mensaje' => "We have availability for $numPersonas people on $fecha at the following times: " . implode(', ', $horariosDisponibles)
        ]);
    }

} catch (Exception $e) {
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'error' => 'Internal server error'
    ]);
    error_log('Error in availability API: ' . $e->getMessage());
}
```

#### Node.js - Ejemplo Completo

```javascript
/**
 * API de Disponibilidad para WAzion
 * Archivo: /api/disponibilidad.js (Express.js)
 */

const express = require('express');
const router = express.Router();

// Authentication middleware
const verificarAuth = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const expectedToken = 'Bearer api_key_restaurante_123';

    if (authHeader !== expectedToken) {
        return res.status(401).json({
            success: false,
            error: 'Invalid authentication token'
        });
    }
    next();
};

// GET /api/v1/disponibilidad
router.get('/disponibilidad', verificarAuth, async (req, res) => {
    try {
        const { fecha, num_personas, tipo_servicio = 'cena' } = req.query;

        // Validations
        if (!fecha) {
            return res.status(400).json({
                success: false,
                error: 'The fecha parameter is required'
            });
        }

        const numPersonas = parseInt(num_personas) || 0;
        if (numPersonas < 1 || numPersonas > 20) {
            return res.status(400).json({
                success: false,
                error: 'Number of people must be between 1 and 20'
            });
        }

        // YOUR BUSINESS LOGIC: Query real availability
        // const disponibilidad = await consultarDisponibilidadEnBD(fecha, numPersonas, tipo_servicio);

        // Example response:
        const horariosBase = tipo_servicio === 'almuerzo'
            ? ['13:00', '13:30', '14:00', '14:30']
            : ['20:00', '20:30', '21:00', '21:30'];

        // Simulate availability (in production, query DB)
        const horariosDisponibles = horariosBase.filter(() => Math.random() > 0.3);

        if (horariosDisponibles.length === 0) {
            return res.json({
                disponible: false,
                horarios: [],
                mensaje: `Sorry, no availability for ${numPersonas} people on ${fecha}.`
            });
        }

        res.json({
            disponible: true,
            horarios: horariosDisponibles,
            mensaje: `We have availability for ${numPersonas} people on ${fecha} at: ${horariosDisponibles.join(', ')}`
        });

    } catch (error) {
        console.error('Error in availability API:', error);
        res.status(500).json({
            success: false,
            error: 'Internal server error'
        });
    }
});

module.exports = router;
```

#### Python (Flask) - Ejemplo Completo

```python
"""
API de Disponibilidad para WAzion
Archivo: /api/disponibilidad.py (Flask)
"""

from flask import Flask, request, jsonify
from functools import wraps
import random

app = Flask(__name__)

# Authentication decorator
def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        expected_token = 'Bearer api_key_restaurante_123'

        if auth_header != expected_token:
            return jsonify({
                'success': False,
                'error': 'Invalid authentication token'
            }), 401
        return f(*args, **kwargs)
    return decorated

@app.route('/api/v1/disponibilidad', methods=['GET', 'POST'])
@require_auth
def consultar_disponibilidad():
    try:
        # Get parameters
        if request.method == 'GET':
            fecha = request.args.get('fecha')
            num_personas = int(request.args.get('num_personas', 0))
            tipo_servicio = request.args.get('tipo_servicio', 'cena')
        else:
            data = request.get_json() or {}
            fecha = data.get('fecha')
            num_personas = int(data.get('num_personas', 0))
            tipo_servicio = data.get('tipo_servicio', 'cena')

        # Validations
        if not fecha:
            return jsonify({
                'success': False,
                'error': 'The fecha parameter is required'
            }), 400

        if num_personas < 1 or num_personas > 20:
            return jsonify({
                'success': False,
                'error': 'Number of people must be between 1 and 20'
            }), 400

        # YOUR BUSINESS LOGIC: Query real availability
        # disponibilidad = consultar_disponibilidad_bd(fecha, num_personas, tipo_servicio)

        # Example response:
        horarios_base = ['13:00', '13:30', '14:00', '14:30'] if tipo_servicio == 'almuerzo' else ['20:00', '20:30', '21:00', '21:30']

        # Simulate availability
        horarios_disponibles = [h for h in horarios_base if random.random() > 0.3]

        if not horarios_disponibles:
            return jsonify({
                'disponible': False,
                'horarios': [],
                'mensaje': f'Sorry, no availability for {num_personas} people on {fecha}.'
            })

        return jsonify({
            'disponible': True,
            'horarios': horarios_disponibles,
            'mensaje': f'We have availability for {num_personas} people on {fecha} at: {", ".join(horarios_disponibles)}'
        })

    except Exception as e:
        print(f'Error in availability API: {e}')
        return jsonify({
            'success': False,
            'error': 'Internal server error'
        }), 500

if __name__ == '__main__':
    app.run(debug=True)
```

---

## CRM Endpoints Personalizados

### Que son los CRM Endpoints

Los CRM Endpoints conectan WAzion con tu sistema CRM/ERP externo para:

1. **Mostrar informacion del cliente** en el panel lateral cuando un agente abre una conversacion
2. **Proporcionar contexto a la IA** antes de responder a un cliente
3. **Buscar clientes** para vincular manualmente a un numero de telefono
4. **Buscar productos** en tu catalogo

### Tipos de Endpoints Disponibles

| Tipo | Uso | Cuando se llama |
|------|-----|-----------------|
| `sidePanel_CustomerInfo` | Muestra info del cliente en el panel lateral | Cuando el agente abre una conversacion |
| `ai_CustomerInitialInfo` | Da contexto a la IA sobre el cliente | Antes de que la IA responda |
| `sidePanel_CustomerFindToJoin` | Busca clientes para asignar | Cuando el agente busca un cliente para vincular |
| `search_Products` | Busca productos en tu catalogo | Cuando se buscan productos (panel o IA) |
| `globalSearch` | Busqueda global de clientes | Cuando el agente usa el buscador flotante |
| `verify_conversion` | Verifica si un cliente compro | Usado por Seguimiento Inteligente para confirmar conversiones |

### Endpoint verify_conversion

Usado por el sistema de Seguimiento Inteligente. WAzion envia el telefono del cliente y un rango de fechas, y tu endpoint responde si hubo compra.

**Request que envia WAzion:**
```json
{
  "phone": "+34612345678",
  "from_date": "2026-02-12T10:00:00Z",
  "to_date": "2026-02-13T10:00:00Z"
}
```

**Response esperada:**
```json
{
  "purchased": true,
  "order_id": "ORD-12345",
  "amount": 29.99,
  "currency": "EUR"
}
```

Si `purchased` es `false` o el endpoint no responde, se trata como candidato a seguimiento.

### Estructura JSON de Configuracion CRM

```json
[
  {
    "type": "sidePanel_CustomerInfo",
    "url": "https://tu-crm.com/api/wazion/customer",
    "method": "GET",
    "format": "json",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer tu_token_api"
      }
    }
  },
  {
    "type": "ai_CustomerInitialInfo",
    "url": "https://tu-crm.com/api/wazion/context",
    "method": "GET",
    "format": "json",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer tu_token_api"
      }
    }
  },
  {
    "type": "sidePanel_CustomerFindToJoin",
    "url": "https://tu-crm.com/api/wazion/search-customers",
    "method": "POST",
    "format": "json",
    "auth": {
      "type": "body",
      "fields": {
        "api_key": "tu_api_key"
      }
    }
  },
  {
    "type": "search_Products",
    "url": "https://tu-crm.com/api/wazion/products",
    "method": "POST",
    "format": "json",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer tu_token_api"
      }
    }
  },
  {
    "type": "globalSearch",
    "url": "https://tu-crm.com/api/wazion/global-search",
    "method": "POST",
    "format": "json",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer tu_token_api"
      }
    }
  }
]
```

**Campos de cada endpoint:**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `type` | string | Si | Tipo de endpoint (ver tabla arriba) |
| `url` | string | Si | URL completa de tu API (debe ser HTTPS en produccion) |
| `method` | string | Si | Metodo HTTP: `GET`, `POST`, `PUT`, `DELETE`, `PATCH` |
| `format` | string | No | Formato del body: `json` (default) o `form` (application/x-www-form-urlencoded) |
| `auth` | object | No | Configuracion de autenticacion |
| `auth.type` | string | Si (si auth) | Tipo: `header`, `query`, `basic`, `body` |
| `auth.fields` | object | Si (si auth) | Campos de autenticacion (key-value) |

### Especificaciones Tecnicas de CRM Endpoints

#### Timeouts y Conexion

| Parametro | Valor | Descripcion |
|-----------|-------|-------------|
| Timeout (globalSearch) | 5 segundos | Tiempo maximo para busqueda global de clientes |
| Timeout (otros endpoints) | 10 segundos | Tiempo maximo para el resto de endpoints |
| Connect timeout | 5 segundos | Tiempo maximo para establecer conexion |

**Recomendaciones:**
- Tu endpoint debe responder en **menos de 5 segundos** para una buena experiencia de usuario
- Si necesitas procesar datos pesados, considera cachear la informacion previamente
- globalSearch tiene timeout mas estricto (5s) porque se usa en el buscador flotante donde la rapidez es critica

#### Comportamiento en Errores

A diferencia de las Funciones Personalizadas y Webhooks, los CRM endpoints **NO tienen reintentos automaticos**. Si tu endpoint falla:

- El panel lateral mostrara un mensaje de error
- La IA no recibira contexto del cliente (para `ai_CustomerInitialInfo`)
- El agente puede reintentar manualmente recargando la conversacion

### Implementacion de cada Endpoint

---

#### sidePanel_CustomerInfo

**Proposito:** Mostrar informacion completa del cliente en el panel lateral del agente.

**Parametros que recibe tu endpoint:**

| Parametro | Tipo | Descripcion |
|-----------|------|-------------|
| `phone` | string | Numero de telefono del cliente (formato internacional: +34612345678) |
| `token` | string | Token de la tienda WAzion |
| `relatedphones` | array | Otros telefonos relacionados con este cliente |
| `test` | boolean | `true` si es una prueba de conexion |

---

##### Respuesta Completa Esperada

La respuesta puede incluir multiples clientes, carritos abandonados, pedidos con tracking, y **labels personalizados** para traducir/personalizar las etiquetas de la UI.

```json
{
  "found": true,
  "labels": {
    "label_languages": "es",
    "customer_info": "Informacion del Cliente",
    "name": "Nombre",
    "email": "Correo electronico",
    "phone": "Telefono",
    "orders_count": "Numero de pedidos",
    "total_spent": "Total gastado",
    "total_refunded": "Total reembolsado",
    "discounts": "Descuentos aplicados",
    "customer_state": "Estado del cliente",
    "recent_orders": "Pedidos recientes",
    "order": "Pedido",
    "date": "Fecha",
    "amount": "Importe",
    "net_amount": "Importe neto",
    "discount": "descuento",
    "refunded": "Reembolsado",
    "cancelled": "Cancelado",
    "items": "Productos",
    "shipments": "Envios",
    "tracking_number": "Numero de seguimiento",
    "order_note": "Nota del pedido",
    "fully_refunded": "Reembolsado",
    "partially_refunded": "Reembolso parcial",
    "abandoned_carts": "Carritos abandonados",
    "copy_link": "Copiar enlace",
    "view_customer": "Ver cliente en CRM",
    "remove_assignment": "Quitar asignacion",
    "not_found": "Cliente no encontrado",
    "error": "Error"
  },
  "customers": [
    {
      "name": "Juan Garcia Lopez",
      "email": "juan.garcia@email.com",
      "phone": "+34612345678",
      "shopUrl": "https://tu-crm.com/clientes/12345",
      "ordersCount": 5,
      "totalSpent": 450.50,
      "totalRefunded": 25.00,
      "totalDiscounts": 15.00,
      "state": "Cliente VIP - Activo",
      "orders": [
        {
          "name": "#ORD-2025-001234",
          "date": "2025-01-10T14:30:00Z",
          "amount": 129.99,
          "netAmount": 104.99,
          "totalRefunded": 25.00,
          "totalDiscounts": 0,
          "currency": "EUR",
          "cancelled": false,
          "shopUrl": "https://tu-crm.com/pedidos/1234",
          "note": "Entregar por la tarde, llamar antes",
          "items": [
            {
              "title": "Camiseta Premium Azul - Talla L",
              "quantity": 2,
              "currentQuantity": 2,
              "amount": 59.98,
              "originalAmount": 59.98,
              "discountedAmount": 59.98,
              "discount": 0,
              "refundedQuantity": 0,
              "refundedAmount": 0,
              "status": "active"
            },
            {
              "title": "Pantalon Casual Negro - Talla 42",
              "quantity": 1,
              "currentQuantity": 0,
              "amount": 70.01,
              "originalAmount": 70.01,
              "discountedAmount": 70.01,
              "discount": 0,
              "refundedQuantity": 1,
              "refundedAmount": 25.00,
              "status": "fully_refunded"
            }
          ],
          "tracking": [
            {
              "number": "ES123456789012",
              "company": "SEUR",
              "url": "https://www.seur.com/livetracking/?segOnlineIdentificador=ES123456789012",
              "displayStatus": "Entregado"
            }
          ]
        },
        {
          "name": "#ORD-2024-009876",
          "date": "2024-12-20T10:15:00Z",
          "amount": 89.99,
          "netAmount": 89.99,
          "totalRefunded": 0,
          "totalDiscounts": 10.00,
          "currency": "EUR",
          "cancelled": false,
          "shopUrl": "https://tu-crm.com/pedidos/9876",
          "note": null,
          "items": [
            {
              "title": "Sudadera con Capucha - Talla M",
              "quantity": 1,
              "currentQuantity": 1,
              "amount": 79.99,
              "originalAmount": 89.99,
              "discountedAmount": 79.99,
              "discount": 10.00,
              "refundedQuantity": 0,
              "refundedAmount": 0,
              "status": "active"
            }
          ],
          "tracking": [
            {
              "number": "MRW987654321",
              "company": "MRW",
              "url": "https://www.mrw.es/seguimiento_envios/?numero=MRW987654321",
              "displayStatus": "En transito"
            }
          ]
        }
      ],
      "abandonedCarts": [
        {
          "date": "2025-01-14T18:45:00Z",
          "url": "https://tu-tienda.com/checkout/recover/abc123xyz",
          "items": [
            {
              "title": "Zapatillas Running Pro",
              "quantity": 1,
              "amount": 129.99
            },
            {
              "title": "Calcetines Deportivos Pack x3",
              "quantity": 2,
              "amount": 15.99
            }
          ]
        }
      ]
    }
  ]
}
```

---

##### Descripcion de Campos

**Nivel raiz:**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `found` | boolean | Si | `true` si se encontro al menos un cliente |
| `labels` | object | No | Etiquetas personalizadas para la UI (ver tabla abajo) |
| `customers` | array | Si* | Array de clientes (soporta multiples) |
| `customer` | object | Si* | Cliente unico (alternativa a `customers`) |
| `error` | string | No | Mensaje de error si `found` es `false` |

*Debe incluir `customers` (array) O `customer` (objeto), no ambos.

---

##### Labels Personalizables

Si incluyes el objeto `labels`, puedes personalizar todas las etiquetas que se muestran en el panel. Si no incluyes un label, WAzion usara el valor por defecto en el idioma de la tienda.

> **Traduccion automatica de labels:** Si especificas `label_languages` con un idioma diferente al de la tienda, WAzion traducira automaticamente TODOS los labels que envies. Por ejemplo, si tu CRM envia labels en ingles (`label_languages: "en"`) pero la tienda esta configurada en espanol, WAzion traducira "Name" a "Nombre", "Email" a "Correo electronico", etc. Esto te permite mantener tu CRM en un solo idioma y dejar que WAzion se encargue de las traducciones.

| Key | Default (ES) | Descripcion |
|-----|--------------|-------------|
| `label_languages` | "es" | Idioma en que envias los labels. Si difiere del idioma de la tienda, WAzion los traduce automaticamente |
| `customer_info` | "Informacion del Cliente" | Titulo de la seccion del cliente |
| `name` | "Nombre" | Etiqueta para nombre |
| `email` | "Email" | Etiqueta para email |
| `phone` | "Telefono" | Etiqueta para telefono |
| `orders_count` | "Pedidos" | Etiqueta para contador de pedidos |
| `total_spent` | "Total gastado" | Etiqueta para total gastado |
| `total_refunded` | "Total reembolsado" | Etiqueta para reembolsos |
| `discounts` | "Descuentos" | Etiqueta para descuentos |
| `customer_state` | "Estado" | Etiqueta para estado del cliente |
| `recent_orders` | "Pedidos recientes" | Titulo de seccion pedidos |
| `order` | "Pedido" | Etiqueta para pedido individual |
| `date` | "Fecha" | Etiqueta para fecha |
| `amount` | "Importe" | Etiqueta para importe |
| `net_amount` | "Importe neto" | Etiqueta para importe neto |
| `discount` | "descuento" | Texto para descuento |
| `refunded` | "Reembolsado" | Etiqueta/badge para reembolsado |
| `cancelled` | "Cancelado" | Badge para pedido cancelado |
| `items` | "Productos" | Titulo de seccion productos |
| `shipments` | "Envios" | Titulo de seccion envios |
| `tracking_number` | "Numero de seguimiento" | Etiqueta para tracking |
| `order_note` | "Nota del pedido" | Etiqueta para notas |
| `fully_refunded` | "Reembolsado" | Badge item totalmente reembolsado |
| `partially_refunded` | "Reembolso parcial" | Badge item parcialmente reembolsado |
| `abandoned_carts` | "Carritos abandonados" | Titulo seccion carritos |
| `copy_link` | "Copiar enlace" | Boton copiar enlace carrito |
| `view_customer` | "Ver cliente" | Boton ver cliente en CRM |
| `remove_assignment` | "Quitar asignacion" | Boton desvincular telefono |
| `not_found` | "No encontrado" | Mensaje cliente no encontrado |
| `error` | "Error" | Prefijo de error |

---

##### Estructura del Cliente (customer)

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `name` | string | Si | Nombre completo del cliente |
| `email` | string | Si | Email del cliente |
| `phone` | string | Si | Telefono principal |
| `shopUrl` | string | No | URL para ver el cliente en tu CRM |
| `ordersCount` | number | Si | Numero total de pedidos |
| `totalSpent` | number | Si | Total gastado historico |
| `totalRefunded` | number | No | Total reembolsado |
| `totalDiscounts` | number | No | Total de descuentos aplicados |
| `state` | string | Si | Estado/categoria del cliente (ej: "VIP", "Nuevo", "Inactivo") |
| `orders` | array | No | Array de pedidos (ver estructura abajo) |
| `abandonedCarts` | array | No | Array de carritos abandonados |

---

##### Estructura del Pedido (order)

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `name` | string | Si | Identificador del pedido (ej: "#1234") |
| `date` | string | Si | Fecha ISO 8601 (ej: "2025-01-10T14:30:00Z") |
| `amount` | number | Si | Importe total del pedido |
| `netAmount` | number | No | Importe neto (despues de reembolsos) |
| `totalRefunded` | number | No | Total reembolsado en este pedido |
| `totalDiscounts` | number | No | Total de descuentos en este pedido |
| `currency` | string | No | Codigo de moneda (default: "EUR") |
| `cancelled` | boolean | No | `true` si el pedido esta cancelado |
| `shopUrl` | string | No | URL para ver el pedido en tu sistema |
| `note` | string | No | Nota/comentario del pedido |
| `items` | array | Si | Array de productos del pedido |
| `tracking` | array | No | Array de envios/tracking |

---

##### Estructura del Item (producto en pedido)

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `title` | string | Si | Nombre del producto |
| `quantity` | number | Si | Cantidad original pedida |
| `currentQuantity` | number | No | Cantidad actual (despues de reembolsos) |
| `amount` | number | Si | Precio total del item |
| `originalAmount` | number | No | Precio original (antes de descuento) |
| `discountedAmount` | number | No | Precio con descuento aplicado |
| `discount` | number | No | Importe del descuento |
| `refundedQuantity` | number | No | Unidades reembolsadas |
| `refundedAmount` | number | No | Importe reembolsado |
| `status` | string | No | Estado: `active`, `partially_refunded`, `fully_refunded` |

---

##### Estructura del Tracking (envio)

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `number` | string | Si | Numero de seguimiento |
| `company` | string | Si | Nombre de la empresa de envio |
| `url` | string | Si | URL de tracking (enlace directo) |
| `displayStatus` | string | Si | Estado legible (ej: "En transito", "Entregado") |

---

##### Estructura del Carrito Abandonado

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `date` | string | Si | Fecha ISO 8601 del abandono |
| `url` | string | Si | URL para recuperar el carrito |
| `items` | array | Si | Array de productos en el carrito |
| `items[].title` | string | Si | Nombre del producto |
| `items[].quantity` | number | Si | Cantidad |
| `items[].amount` | number | Si | Precio unitario |

---

##### Respuesta cuando NO se encuentra cliente

```json
{
  "found": false,
  "error": "No customer found with that phone number"
}
```

**IMPORTANTE:** No devuelvas codigo HTTP 404. Siempre devuelve 200 con `"found": false`.

---

##### Implementacion PHP Completa

```php
<?php
/**
 * Endpoint: sidePanel_CustomerInfo
 * Archivo: /api/wazion/customer.php
 */

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Verificar autenticacion
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($authHeader !== 'Bearer tu_token_api') {
    http_response_code(401);
    echo json_encode(['found' => false, 'error' => 'Unauthorized']);
    exit;
}

// Get parameters (supports GET and POST/JSON)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
} else {
    $input = $_GET;
}

$phone = $input['phone'] ?? null;
$token = $input['token'] ?? null;
$relatedPhones = $input['relatedphones'] ?? [];
$isTest = filter_var($input['test'] ?? false, FILTER_VALIDATE_BOOLEAN);

if (!$phone) {
    echo json_encode(['found' => false, 'error' => 'Phone required']);
    exit;
}

// Normalizar telefono
$phoneNormalized = preg_replace('/[^\d+]/', '', $phone);

// Connection test
if ($isTest) {
    echo json_encode([
        'found' => true,
        'labels' => [
            'label_languages' => 'en',
            'customer_info' => 'Connection Test Successful'
        ],
        'customers' => [[
            'name' => 'Test Customer',
            'email' => 'test@example.com',
            'phone' => $phone,
            'shopUrl' => 'https://tu-crm.com/test',
            'ordersCount' => 0,
            'totalSpent' => 0,
            'state' => 'Connection OK'
        ]]
    ]);
    exit;
}

// LOGICA DE TU NEGOCIO
try {
    $pdo = new PDO('mysql:host=localhost;dbname=tu_db;charset=utf8mb4', 'usuario', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Buscar cliente por telefono (incluir relacionados)
    $phonesToSearch = array_merge([$phoneNormalized], $relatedPhones);
    $placeholders = implode(',', array_fill(0, count($phonesToSearch), '?'));

    $stmt = $pdo->prepare("
        SELECT c.*,
               COUNT(DISTINCT p.id) as orders_count,
               COALESCE(SUM(p.total), 0) as total_spent,
               COALESCE(SUM(p.total_reembolsado), 0) as total_refunded
        FROM clientes c
        LEFT JOIN pedidos p ON c.id = p.cliente_id
        WHERE c.telefono IN ($placeholders)
        GROUP BY c.id
    ");
    $stmt->execute($phonesToSearch);
    $clientes = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if (empty($clientes)) {
        echo json_encode(['found' => false]);
        exit;
    }

    $customers = [];

    foreach ($clientes as $cliente) {
        // Obtener pedidos
        $stmtPedidos = $pdo->prepare("
            SELECT * FROM pedidos
            WHERE cliente_id = ?
            ORDER BY fecha DESC
            LIMIT 10
        ");
        $stmtPedidos->execute([$cliente['id']]);
        $pedidos = $stmtPedidos->fetchAll(PDO::FETCH_ASSOC);

        $orders = [];
        foreach ($pedidos as $pedido) {
            // Obtener items del pedido
            $stmtItems = $pdo->prepare("SELECT * FROM pedido_items WHERE pedido_id = ?");
            $stmtItems->execute([$pedido['id']]);
            $items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);

            $orderItems = [];
            foreach ($items as $item) {
                $status = 'active';
                if ($item['cantidad_reembolsada'] >= $item['cantidad']) {
                    $status = 'fully_refunded';
                } elseif ($item['cantidad_reembolsada'] > 0) {
                    $status = 'partially_refunded';
                }

                $orderItems[] = [
                    'title' => $item['nombre'],
                    'quantity' => (int)$item['cantidad'],
                    'currentQuantity' => (int)($item['cantidad'] - $item['cantidad_reembolsada']),
                    'amount' => (float)$item['precio_total'],
                    'originalAmount' => (float)($item['precio_original'] ?? $item['precio_total']),
                    'discountedAmount' => (float)$item['precio_total'],
                    'discount' => (float)($item['descuento'] ?? 0),
                    'refundedQuantity' => (int)($item['cantidad_reembolsada'] ?? 0),
                    'refundedAmount' => (float)($item['importe_reembolsado'] ?? 0),
                    'status' => $status
                ];
            }

            // Obtener tracking
            $stmtTracking = $pdo->prepare("SELECT * FROM pedido_envios WHERE pedido_id = ?");
            $stmtTracking->execute([$pedido['id']]);
            $envios = $stmtTracking->fetchAll(PDO::FETCH_ASSOC);

            $tracking = [];
            foreach ($envios as $envio) {
                $tracking[] = [
                    'number' => $envio['numero_seguimiento'],
                    'company' => $envio['empresa'],
                    'url' => $envio['url_tracking'],
                    'displayStatus' => $envio['estado_texto']
                ];
            }

            $orders[] = [
                'name' => '#' . $pedido['numero_pedido'],
                'date' => date('c', strtotime($pedido['fecha'])),
                'amount' => (float)$pedido['total'],
                'netAmount' => (float)($pedido['total'] - ($pedido['total_reembolsado'] ?? 0)),
                'totalRefunded' => (float)($pedido['total_reembolsado'] ?? 0),
                'totalDiscounts' => (float)($pedido['descuentos'] ?? 0),
                'currency' => $pedido['moneda'] ?? 'EUR',
                'cancelled' => (bool)$pedido['cancelado'],
                'shopUrl' => 'https://tu-crm.com/pedidos/' . $pedido['id'],
                'note' => $pedido['notas'] ?: null,
                'items' => $orderItems,
                'tracking' => $tracking
            ];
        }

        // Obtener carritos abandonados
        $stmtCarts = $pdo->prepare("
            SELECT * FROM carritos_abandonados
            WHERE cliente_id = ? AND recuperado = 0
            ORDER BY fecha DESC LIMIT 5
        ");
        $stmtCarts->execute([$cliente['id']]);
        $carritos = $stmtCarts->fetchAll(PDO::FETCH_ASSOC);

        $abandonedCarts = [];
        foreach ($carritos as $carrito) {
            $stmtCartItems = $pdo->prepare("SELECT * FROM carrito_items WHERE carrito_id = ?");
            $stmtCartItems->execute([$carrito['id']]);
            $cartItems = $stmtCartItems->fetchAll(PDO::FETCH_ASSOC);

            $items = [];
            foreach ($cartItems as $ci) {
                $items[] = [
                    'title' => $ci['nombre_producto'],
                    'quantity' => (int)$ci['cantidad'],
                    'amount' => (float)$ci['precio']
                ];
            }

            $abandonedCarts[] = [
                'date' => date('c', strtotime($carrito['fecha'])),
                'url' => $carrito['url_recuperacion'],
                'items' => $items
            ];
        }

        $customers[] = [
            'name' => $cliente['nombre'],
            'email' => $cliente['email'],
            'phone' => $cliente['telefono'],
            'shopUrl' => 'https://tu-crm.com/clientes/' . $cliente['id'],
            'ordersCount' => (int)$cliente['orders_count'],
            'totalSpent' => (float)$cliente['total_spent'],
            'totalRefunded' => (float)$cliente['total_refunded'],
            'totalDiscounts' => (float)($cliente['total_descuentos'] ?? 0),
            'state' => $cliente['estado'] ?? 'Cliente',
            'orders' => $orders,
            'abandonedCarts' => $abandonedCarts
        ];
    }

    // Respuesta con labels personalizados (opcional)
    echo json_encode([
        'found' => true,
        'labels' => [
            'label_languages' => 'es',
            // Puedes personalizar cualquier label aqui
            // Si omites labels, WAzion usa los defaults
        ],
        'customers' => $customers
    ], JSON_UNESCAPED_UNICODE);

} catch (Exception $e) {
    error_log('Error en sidePanel_CustomerInfo: ' . $e->getMessage());
    echo json_encode(['found' => false, 'error' => 'Internal server error']);
}
```

---

#### ai_CustomerInitialInfo

**Proposito:** Proporcionar contexto textual a la IA sobre el cliente ANTES de que responda. Este contexto ayuda a la IA a personalizar sus respuestas.

**Parametros que recibe tu endpoint:**

| Parametro | Tipo | Descripcion |
|-----------|------|-------------|
| `phone` | string | Numero de telefono del cliente (formato internacional) |
| `token` | string | Token de la tienda WAzion |
| `relatedphones` | array | Otros telefonos relacionados con este cliente |
| `order` | string | Numero de pedido si el cliente lo menciono (opcional) |
| `email` | string | Email del cliente si se conoce (opcional) |
| `test` | boolean | `true` si es una prueba de conexion |

**Respuesta esperada:**

```json
{
  "found": true,
  "info": "Cliente habitual desde hace 2 anios. Ultima compra hace 1 semana (Producto X por 45EUR). Total historico: 1,250EUR en 15 pedidos. Preferencias: Envio express, pago con tarjeta. Notas: Le gustan los productos premium, sensible al precio en accesorios. IMPORTANTE: Cliente alergico al gluten."
}
```

**Campos de respuesta:**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `found` | boolean | Si | `true` si se encontro informacion del cliente |
| `info` | string | Si (si found=true) | Texto libre con contexto para la IA |

**IMPORTANTE sobre el campo `info`:**

El campo `info` es texto libre que la IA usara como contexto. Incluye informacion relevante y estructurada para que la IA personalice sus respuestas. Recomendaciones:

- Incluir historial de compras resumido
- Preferencias del cliente
- Alertas importantes (alergias, problemas previos, etc.)
- Estado del cliente (VIP, nuevo, inactivo)
- Ultima interaccion
- Notas del comerciante
- Productos favoritos
- Metodo de pago/envio preferido

**Implementacion PHP:**

```php
<?php
/**
 * Endpoint: ai_CustomerInitialInfo
 * Archivo: /api/wazion/context.php
 */

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Authentication
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($authHeader !== 'Bearer tu_token_api') {
    http_response_code(401);
    echo json_encode(['found' => false, 'error' => 'Unauthorized']);
    exit;
}

// Get parameters
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
} else {
    $input = $_GET;
}

$phone = $input['phone'] ?? null;
$token = $input['token'] ?? null;
$relatedPhones = $input['relatedphones'] ?? [];
$isTest = filter_var($input['test'] ?? false, FILTER_VALIDATE_BOOLEAN);

if (!$phone) {
    echo json_encode(['found' => false]);
    exit;
}

// Connection test
if ($isTest) {
    echo json_encode([
        'found' => true,
        'info' => 'Connection test successful. This is an example of the context that the AI will receive about the customer.'
    ]);
    exit;
}

// LOGICA DE TU NEGOCIO: Obtener contexto del cliente
try {
    $pdo = new PDO('mysql:host=localhost;dbname=tu_db;charset=utf8mb4', 'usuario', 'password');

    // Buscar cliente
    $phoneNormalized = preg_replace('/[^\d+]/', '', $phone);
    $stmt = $pdo->prepare("SELECT * FROM clientes WHERE telefono = ?");
    $stmt->execute([$phoneNormalized]);
    $cliente = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$cliente) {
        echo json_encode(['found' => false]);
        exit;
    }

    // Obtener estadisticas
    $stmt = $pdo->prepare("
        SELECT COUNT(*) as total_pedidos,
               SUM(total) as total_gastado,
               MAX(fecha) as ultima_compra
        FROM pedidos WHERE cliente_id = ?
    ");
    $stmt->execute([$cliente['id']]);
    $stats = $stmt->fetch(PDO::FETCH_ASSOC);

    // Obtener ultimo pedido
    $stmt = $pdo->prepare("
        SELECT p.*, GROUP_CONCAT(i.nombre SEPARATOR ', ') as productos
        FROM pedidos p
        LEFT JOIN pedido_items i ON p.id = i.pedido_id
        WHERE p.cliente_id = ?
        ORDER BY p.fecha DESC LIMIT 1
    ");
    $stmt->execute([$cliente['id']]);
    $ultimoPedido = $stmt->fetch(PDO::FETCH_ASSOC);

    // Construir contexto enriquecido
    $contexto = [];

    // Info basica
    $contexto[] = "Cliente: {$cliente['nombre']}.";

    // Estado/categoria
    if (!empty($cliente['categoria'])) {
        $contexto[] = "Categoria: {$cliente['categoria']}.";
    }

    // Historial
    $totalPedidos = (int)$stats['total_pedidos'];
    $totalGastado = number_format((float)$stats['total_gastado'], 2);
    if ($totalPedidos > 0) {
        $contexto[] = "Historial: {$totalPedidos} pedidos por {$totalGastado} EUR en total.";
    } else {
        $contexto[] = "Cliente nuevo, sin pedidos anteriores.";
    }

    // Ultima compra
    if ($ultimoPedido) {
        $diasDesdeUltimaCompra = floor((time() - strtotime($ultimoPedido['fecha'])) / 86400);
        $contexto[] = "Ultima compra: hace {$diasDesdeUltimaCompra} dias ({$ultimoPedido['productos']}).";
    }

    // Preferencias
    if (!empty($cliente['metodo_pago_preferido'])) {
        $contexto[] = "Pago preferido: {$cliente['metodo_pago_preferido']}.";
    }
    if (!empty($cliente['metodo_envio_preferido'])) {
        $contexto[] = "Envio preferido: {$cliente['metodo_envio_preferido']}.";
    }

    // Notas importantes (CRITICO)
    if (!empty($cliente['notas_importantes'])) {
        $contexto[] = "IMPORTANTE: {$cliente['notas_importantes']}";
    }

    // Alertas (alergias, etc.)
    if (!empty($cliente['alertas'])) {
        $contexto[] = "ALERTA: {$cliente['alertas']}";
    }

    echo json_encode([
        'found' => true,
        'info' => implode(' ', $contexto)
    ], JSON_UNESCAPED_UNICODE);

} catch (Exception $e) {
    error_log('Error en ai_CustomerInitialInfo: ' . $e->getMessage());
    echo json_encode(['found' => false]);
}
```

---

#### sidePanel_CustomerFindToJoin

**Proposito:** Buscar clientes en tu CRM para vincularlos manualmente a un numero de WhatsApp. Se usa cuando un agente quiere asignar una conversacion a un cliente existente.

**Parametros que recibe tu endpoint:**

| Parametro | Tipo | Descripcion |
|-----------|------|-------------|
| `query` | string | Termino de busqueda (nombre, email, telefono, ID) |
| `token` | string | Token de la tienda WAzion |
| `phone` | string | Telefono actual de la conversacion |
| `relatedphones` | array | Telefonos ya relacionados (para excluirlos) |
| `test` | boolean | `true` si es una prueba de conexion |

**Respuesta esperada:**

Puedes usar `results` O `customers` como nombre del array (ambos son validos):

```json
{
  "results": [
    {
      "id": "CRM-12345",
      "name": "Juan Garcia Lopez",
      "email": "juan.garcia@email.com",
      "phone": "+34611111111"
    },
    {
      "id": "CRM-12346",
      "name": "Juan Martinez Perez",
      "email": "juan.martinez@email.com",
      "phone": "+34622222222"
    }
  ]
}
```

**Campos de cada resultado:**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `id` | string | Si | ID unico del cliente en tu CRM |
| `name` | string | Si | Nombre completo del cliente |
| `email` | string | Si | Email del cliente |
| `phone` | string | Si | Telefono del cliente |

**Implementacion PHP:**

```php
<?php
/**
 * Endpoint: sidePanel_CustomerFindToJoin
 * Archivo: /api/wazion/search-customers.php
 */

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Obtener parametros
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
} else {
    $input = $_GET;
}

$query = trim($input['query'] ?? '');
$token = $input['token'] ?? '';
$phone = $input['phone'] ?? '';
$relatedPhones = $input['relatedphones'] ?? [];
$isTest = filter_var($input['test'] ?? false, FILTER_VALIDATE_BOOLEAN);

// Verify authentication (according to your auth configuration)
$apiKey = $input['api_key'] ?? '';
if ($apiKey !== 'tu_api_key') {
    http_response_code(401);
    echo json_encode(['results' => [], 'error' => 'Unauthorized']);
    exit;
}

// Connection test
if ($isTest) {
    echo json_encode([
        'results' => [
            [
                'id' => 'TEST-001',
                'name' => 'Test Customer',
                'email' => 'test@example.com',
                'phone' => '+34600000000'
            ]
        ]
    ]);
    exit;
}

// Validar query minimo
if (strlen($query) < 2) {
    echo json_encode(['results' => []]);
    exit;
}

try {
    $pdo = new PDO('mysql:host=localhost;dbname=tu_db;charset=utf8mb4', 'usuario', 'password');

    // Construir exclusiones (telefonos ya relacionados)
    $excludeClause = '';
    $params = [];

    if (!empty($relatedPhones)) {
        $placeholders = implode(',', array_fill(0, count($relatedPhones), '?'));
        $excludeClause = "AND telefono NOT IN ($placeholders)";
        $params = $relatedPhones;
    }

    // Buscar por nombre, email o telefono
    $searchTerm = "%{$query}%";
    $sql = "
        SELECT id, nombre, email, telefono
        FROM clientes
        WHERE (nombre LIKE ? OR email LIKE ? OR telefono LIKE ? OR id LIKE ?)
        $excludeClause
        ORDER BY nombre ASC
        LIMIT 15
    ";

    $stmt = $pdo->prepare($sql);
    $stmt->execute(array_merge([$searchTerm, $searchTerm, $searchTerm, $searchTerm], $params));
    $clientes = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $results = [];
    foreach ($clientes as $cliente) {
        $results[] = [
            'id' => (string)$cliente['id'],
            'name' => $cliente['nombre'],
            'email' => $cliente['email'] ?? '',
            'phone' => $cliente['telefono'] ?? ''
        ];
    }

    // Puedes usar 'results' o 'customers' - ambos funcionan
    echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE);

} catch (Exception $e) {
    error_log('Error en sidePanel_CustomerFindToJoin: ' . $e->getMessage());
    echo json_encode(['results' => [], 'error' => 'Search error']);
}
```

---

#### search_Products

**Proposito:** Buscar productos en tu catalogo. Se usa en el buscador de productos del panel lateral y por la IA cuando necesita informacion de productos.

**Parametros que recibe tu endpoint:**

| Parametro | Tipo | Descripcion |
|-----------|------|-------------|
| `q` | string | Termino de busqueda (minimo 2 caracteres) |
| `token` | string | Token de la tienda WAzion |
| `phone` | string | Telefono del cliente E.164 (para URLs localizadas por pais) |
| `format` | string | `"ai"` cuando la IA busca productos (cambia estructura de respuesta) |
| `target_locale` | string | Idioma detectado del cliente basado en su telefono (ej: "es", "de", "fr"). Util para devolver titulos/descripciones en el idioma del cliente |
| `test` | boolean | `true` si es una prueba de conexion |

> **Nuevo: target_locale** - WAzion detecta automaticamente el idioma del cliente basandose en el prefijo de su numero de telefono. Por ejemplo, un cliente con +34... recibira `target_locale: "es"`, mientras que uno con +49... recibira `target_locale: "de"`. Puedes usar este parametro para devolver productos con titulos y descripciones en el idioma correcto.

---

> **NOTA IMPORTANTE para CRM Custom:** WAzion actua como proxy transparente para este endpoint. La respuesta de tu CRM se devuelve directamente a la extension sin modificar. Debes implementar la estructura completa documentada a continuacion si quieres aprovechar todas las funcionalidades. La estructura enriquecida con `target_locale`, `title_copy`, `raw`, etc. solo se genera automaticamente para tiendas con una plataforma e-commerce conectada (Shopify, WooCommerce, PrestaShop, VTEX).

---

**RESPUESTA FORMATO NORMAL (Panel Lateral):**

Cuando el agente busca productos desde el panel. Incluye campos para copiar/compartir.

```json
{
  "http_status": 200,
  "count": 1,
  "products": [
    {
      "title": "Camiseta Premium",
      "title_copy": "Premium T-Shirt",
      "handle": "camiseta-premium",
      "description": "Descripcion truncada a 500 caracteres...",
      "sku": "CAM-001",
      "available": true,
      "image": "https://proxy/imagen.jpg",
      "url": "https://tutienda.es/products/camiseta-premium",
      "variants": [
        {
          "id": "12345678",
          "title": "M / Rojo",
          "title_copy": "M / Red",
          "has_custom_title": true,
          "sku": "CAM-001-M-RED",
          "available": true,
          "image": "https://proxy/variante.jpg",
          "url": "https://tutienda.es/products/camiseta-premium?variant=12345678"
        }
      ]
    }
  ],
  "target_locale": "en",
  "raw": { ... }
}
```

---

**RESPUESTA FORMATO AI (cuando format="ai"):**

Cuando la IA busca productos. Estructura simplificada, descripcion completa, sin campos de copia.

```json
{
  "count": 1,
  "products": [
    {
      "title": "Premium T-Shirt",
      "handle": "camiseta-premium",
      "description": "Descripcion COMPLETA sin truncar para contexto IA...",
      "sku": "CAM-001",
      "available": true,
      "url": "https://tutienda.es/products/camiseta-premium",
      "variants": [
        {
          "id": "12345678",
          "title": "M / Red",
          "sku": "CAM-001-M-RED",
          "available": true,
          "url": "https://tutienda.es/products/camiseta-premium?variant=12345678"
        }
      ]
    }
  ],
  "locale": "en"
}
```

---

**Diferencias entre formatos:**

| Campo | Formato normal (panel) | Formato AI |
|-------|------------------------|------------|
| `title` | Titulo original | Titulo traducido al idioma del cliente |
| `title_copy` | Titulo traducido (para copiar) | **No incluido** |
| `description` | Truncada (500 chars) | Completa (para contexto IA) |
| `available` | Incluido | Incluido |
| `image` | Incluida | **No incluida** |
| `http_status` | Incluido | **No incluido** |
| `target_locale` | Incluido | - |
| `locale` | - | Incluido |
| `raw` | Incluido (debug) | **No incluido** |
| `variants[].title` | Titulo original | Titulo traducido |
| `variants[].title_copy` | Titulo traducido | **No incluido** |
| `variants[].has_custom_title` | Incluido | **No incluido** |
| `variants[].image` | Incluida | **No incluida** |

---

**Campos de la respuesta raiz:**

| Campo | Tipo | Normal | AI | Descripcion |
|-------|------|--------|-----|-------------|
| `http_status` | int | Si | No | Codigo HTTP de la fuente |
| `count` | int | Si | Si | Numero de productos encontrados |
| `products` | array | Si | Si | Array de productos |
| `target_locale` | string | Si | No | Locale usado para traducciones |
| `locale` | string | No | Si | Locale usado (solo formato AI) |
| `raw` | object | Si | No | Respuesta raw de la fuente (debug) |

**Campos de cada producto:**

| Campo | Tipo | Normal | AI | Descripcion |
|-------|------|--------|-----|-------------|
| `title` | string | Original | Traducido | Titulo del producto |
| `title_copy` | string | Traducido | - | Titulo para copiar (idioma del cliente) |
| `handle` | string | Si | Si | Slug/identificador del producto |
| `description` | string | 500 chars | Completa | Descripcion (HTML limpiado) |
| `sku` | string | Si | Si | SKU de la primera variante |
| `available` | boolean | Si | Si | Disponibilidad del producto: true si activo y al menos una variante tiene stock (opcional) |
| `image` | string | Si | - | URL de imagen (puede ser proxied) |
| `url` | string | Si | Si | URL localizada del producto |
| `variants` | array | Si | Si | Array de variantes |

**Campos de cada variante:**

| Campo | Tipo | Normal | AI | Descripcion |
|-------|------|--------|-----|-------------|
| `id` | string | Si | Si | ID de la variante |
| `title` | string | Original | Traducido | Titulo de variante (ej: "M / Rojo") |
| `title_copy` | string | Traducido | - | Titulo para copiar |
| `has_custom_title` | boolean | Si | - | true si no es "Default Title" |
| `sku` | string | Si | Si | SKU de la variante |
| `available` | boolean | Si | Si | Disponibilidad de la variante (opcional) |
| `image` | string | Si | - | Imagen de la variante |
| `url` | string | Si | Si | URL con ?variant=ID |

---

**URLs localizadas por pais:**

El parametro `phone` contiene el telefono del cliente (E.164). Usalo para detectar su pais y devolver URLs adaptadas:

- Dominios especificos por pais (tutienda.es, tutienda.de, tutienda.fr)
- Rutas con prefijo de idioma (/es/, /de/, /fr/)
- Parametros de locale (?locale=es_ES)
- Markets de tu plataforma e-commerce (Shopify Markets, WooCommerce WPML, PrestaShop multitienda, etc.)

Ejemplo: Si el telefono empieza por +34 (Espana), devolver URLs con dominio .es o /es/.

**Implementacion PHP:**

```php
<?php
/**
 * Endpoint: search_Products
 * Archivo: /api/wazion/products.php
 */

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Obtener parametros
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
} else {
    $input = $_GET;
}

$query = trim($input['q'] ?? $input['query'] ?? '');
$token = $input['token'] ?? '';
$isTest = filter_var($input['test'] ?? false, FILTER_VALIDATE_BOOLEAN);

// Authentication (according to your configuration)
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($authHeader !== 'Bearer tu_token_api') {
    http_response_code(401);
    echo json_encode(['products' => [], 'error' => 'Unauthorized']);
    exit;
}

// Connection test
if ($isTest) {
    echo json_encode([
        'products' => [
            [
                'id' => 'TEST-001',
                'title' => 'Test Product',
                'price' => 99.99,
                'url' => 'https://tu-tienda.com/test',
                'image' => 'https://via.placeholder.com/300',
                'available' => true
            ]
        ]
    ]);
    exit;
}

if (strlen($query) < 2) {
    echo json_encode(['products' => []]);
    exit;
}

try {
    $pdo = new PDO('mysql:host=localhost;dbname=tu_db;charset=utf8mb4', 'usuario', 'password');

    $stmt = $pdo->prepare("
        SELECT p.*,
               GROUP_CONCAT(DISTINCT v.id, ':', v.nombre, ':', v.precio, ':', v.stock SEPARATOR '|') as variantes
        FROM productos p
        LEFT JOIN variantes v ON p.id = v.producto_id
        WHERE (p.nombre LIKE ? OR p.descripcion LIKE ? OR p.sku LIKE ?)
        AND p.activo = 1
        GROUP BY p.id
        ORDER BY p.nombre ASC
        LIMIT 20
    ");

    $searchTerm = "%{$query}%";
    $stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
    $productos = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $results = [];
    foreach ($productos as $prod) {
        $variants = [];

        // Parsear variantes si existen
        if (!empty($prod['variantes'])) {
            foreach (explode('|', $prod['variantes']) as $varStr) {
                $parts = explode(':', $varStr);
                if (count($parts) >= 4) {
                    $variants[] = [
                        'id' => $parts[0],
                        'title' => $parts[1],
                        'price' => (float)$parts[2],
                        'inventory' => (int)$parts[3],
                        'available' => (int)$parts[3] > 0
                    ];
                }
            }
        }

        $product = [
            'id' => (string)$prod['id'],
            'title' => $prod['nombre'],
            'price' => (float)$prod['precio'],
            'url' => $prod['url'] ?? 'https://tu-tienda.com/productos/' . $prod['id'],
            'image' => $prod['imagen_url'] ?? '',
            'available' => (int)($prod['stock'] ?? 0) > 0,
            'inventory' => (int)($prod['stock'] ?? 0),
            'sku' => $prod['sku'] ?? null,
            'vendor' => $prod['marca'] ?? null,
            'productType' => $prod['categoria'] ?? null
        ];

        // Agregar precio anterior si existe
        if (!empty($prod['precio_anterior']) && $prod['precio_anterior'] > $prod['precio']) {
            $product['compareAtPrice'] = (float)$prod['precio_anterior'];
        }

        // Agregar variantes si existen
        if (!empty($variants)) {
            $product['variants'] = $variants;
        }

        $results[] = $product;
    }

    // Puedes usar 'products' o 'results' - ambos funcionan
    echo json_encode(['products' => $results], JSON_UNESCAPED_UNICODE);

} catch (Exception $e) {
    error_log('Error en search_Products: ' . $e->getMessage());
    echo json_encode(['products' => [], 'error' => 'Search error']);
}
```

---

#### globalSearch

**Proposito:** Busqueda global de clientes para el buscador flotante de la extension. Permite buscar por nombre, email, telefono o numero de pedido.

**Parametros que recibe tu endpoint:**

| Parametro | Tipo | Descripcion |
|-----------|------|-------------|
| `query` | string | Termino de busqueda (minimo 2 caracteres) |
| `token` | string | Token de autenticacion de la tienda |

**Respuesta esperada:**

```json
{
  "found": true,
  "customers": [
    {
      "id": "cust_123",
      "name": "Juan Garcia Lopez",
      "email": "juan.garcia@email.com",
      "phone": "+34612345678",
      "customerUrl": "https://tu-crm.com/clientes/123",
      "orders": [
        {
          "number": "#ORD-1234",
          "url": "https://tu-crm.com/pedidos/1234"
        },
        {
          "number": "#ORD-1233",
          "url": "https://tu-crm.com/pedidos/1233"
        }
      ]
    }
  ]
}
```

**Campos de cada cliente:**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `id` | string | Si | ID unico del cliente en tu sistema |
| `name` | string | Si | Nombre completo del cliente |
| `email` | string | Si | Email del cliente |
| `phone` | string | Si | Telefono del cliente (E.164 preferido, ej: +34612345678). **Obligatorio para poder abrir conversacion** |
| `customerUrl` | string | No | URL para ver el cliente en tu CRM (se muestra como enlace clickeable) |
| `orders` | array | No | Array con los ultimos pedidos (maximo 3 seran mostrados) |

**Campos de cada pedido (orders[]):**

| Campo | Tipo | Obligatorio | Descripcion |
|-------|------|-------------|-------------|
| `number` | string | Si | Numero de pedido (ej: "#ORD-1234") |
| `url` | string | No | URL para ver el pedido en tu sistema |

**Notas importantes:**

- El campo `phone` es **obligatorio** para que el agente pueda abrir la conversacion con ese cliente
- Maximo **15 resultados** seran mostrados al usuario (limitado automaticamente)
- Los duplicados por telefono se eliminan automaticamente
- Si `customerUrl` esta presente, el nombre del cliente sera un enlace clickeable
- Los pedidos se muestran como enlaces individuales debajo del cliente

**Implementacion PHP:**

```php
<?php
/**
 * Endpoint: globalSearch
 * Archivo: /api/wazion/global-search.php
 */

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Obtener parametros
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
} else {
    $input = $_GET;
}

$query = trim($input['query'] ?? '');
$token = $input['token'] ?? '';

// Validar token
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($authHeader !== 'Bearer tu_token_api') {
    http_response_code(401);
    echo json_encode(['found' => false, 'customers' => []]);
    exit;
}

// Validar query minimo
if (strlen($query) < 2) {
    echo json_encode(['found' => false, 'customers' => []]);
    exit;
}

try {
    $pdo = new PDO('mysql:host=localhost;dbname=tu_db;charset=utf8mb4', 'usuario', 'password');

    $searchTerm = "%{$query}%";
    $stmt = $pdo->prepare("
        SELECT c.id, c.nombre, c.email, c.telefono
        FROM clientes c
        WHERE c.nombre LIKE ? OR c.email LIKE ? OR c.telefono LIKE ?
        ORDER BY c.nombre ASC
        LIMIT 15
    ");
    $stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
    $clientes = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $customers = [];
    foreach ($clientes as $cliente) {
        // Obtener ultimos 3 pedidos
        $stmtPedidos = $pdo->prepare("
            SELECT numero, id FROM pedidos
            WHERE cliente_id = ?
            ORDER BY fecha DESC LIMIT 3
        ");
        $stmtPedidos->execute([$cliente['id']]);
        $pedidos = $stmtPedidos->fetchAll(PDO::FETCH_ASSOC);

        $orders = [];
        foreach ($pedidos as $p) {
            $orders[] = [
                'number' => '#' . $p['numero'],
                'url' => 'https://tu-crm.com/pedidos/' . $p['id']
            ];
        }

        $customers[] = [
            'id' => (string)$cliente['id'],
            'name' => $cliente['nombre'],
            'email' => $cliente['email'] ?? '',
            'phone' => $cliente['telefono'] ?? '',
            'customerUrl' => 'https://tu-crm.com/clientes/' . $cliente['id'],
            'orders' => $orders
        ];
    }

    echo json_encode([
        'found' => !empty($customers),
        'customers' => $customers
    ], JSON_UNESCAPED_UNICODE);

} catch (Exception $e) {
    error_log('Error en globalSearch: ' . $e->getMessage());
    echo json_encode(['found' => false, 'customers' => []]);
}
```

---

## Alertas Automaticas (Webhooks)

### Como funcionan las Alertas

Las Alertas Automaticas (Webhooks) permiten que WAzion notifique a tu sistema cuando ocurren eventos importantes. WAzion envia una peticion HTTP POST a tu servidor con los datos del evento.

**Caracteristicas:**
- Reintentos automaticos con backoff exponencial (hasta 5 intentos totales)
- Firma HMAC-SHA256 opcional para verificar autenticidad
- Cola de mensajes para garantizar entrega
- Deduplicacion automatica de telefonos (no envia el mismo telefono dos veces)
- Rate limiting: maximo 100 webhooks en cola por tienda
- Logs de entrega para auditoria

**Secuencia de reintentos:**

| Intento | Espera | Tiempo acumulado |
|---------|--------|------------------|
| 1 | 0 segundos | Inmediato |
| 2 | 60 segundos | 1 minuto |
| 3 | 300 segundos | 5 minutos |
| 4 | 900 segundos | 15 minutos |
| 5 | 3600 segundos | 1 hora |
| 6 | 14400 segundos | 4 horas (ultimo) |

**Maximo 5 reintentos** (6 intentos totales). Despues del intento 6, el webhook se marca como `permanent_failure`.

**Codigos HTTP que triggerean reintentos:**

| Codigo | Descripcion |
|--------|-------------|
| 408 | Request Timeout |
| 429 | Too Many Requests |
| 5xx | Cualquier error de servidor (500, 502, 503, 504, etc.) |

**Codigos HTTP que NO reintentan (failure permanente):**

| Codigo | Descripcion |
|--------|-------------|
| 4xx (excepto 408 y 429) | Errores del cliente (400, 401, 403, 404, etc.) |

#### Especificaciones Tecnicas de Envio

| Parametro | Valor |
|-----------|-------|
| `CURLOPT_TIMEOUT` | 180 segundos |
| `CURLOPT_CONNECTTIMEOUT` | 5 segundos |
| `CURLOPT_SSL_VERIFYPEER` | true (verifica certificado SSL) |
| `CURLOPT_SSL_VERIFYHOST` | 2 (verifica CN y SAN del certificado) |
| `CURLOPT_FOLLOWLOCATION` | false (no sigue redirecciones) |
| `CURLOPT_MAXREDIRS` | 0 |

**IMPORTANTE:** Los webhooks NO siguen redirecciones. Tu endpoint debe responder directamente sin redireccionar.

#### Deduplicacion de Telefonos

WAzion almacena un hash SHA256 de cada telefono enviado. Si el mismo telefono ya fue notificado anteriormente, NO se envia de nuevo. Esto evita duplicados en tu sistema.

#### Rate Limiting

- Maximo **100 webhooks** en cola (status `pending` o `processing`) por tienda
- Si se supera el limite, los nuevos webhooks se descartan
- El procesador de cola ejecuta maximo **50 webhooks** por ejecucion
- Delay de **1 segundo** entre cada webhook enviado

### Eventos Disponibles

| Evento | Descripcion |
|--------|-------------|
| `phone.detected` | Se detecto un nuevo numero de telefono en una conversacion |
| `followup.detected` | El Seguimiento Inteligente detecto intencion de compra en una conversacion |
| `followup.replied` | El cliente respondio al mensaje de seguimiento enviado por el Seguimiento Inteligente |
| `followup.converted` | El cliente realizo una compra tras recibir el mensaje de seguimiento |
| `plugin_chat.session_closed` | Una sesion del widget de chat web finalizo por inactividad |
| `test` | Evento de prueba para verificar la conexion |

### Formato del Payload

#### Evento: phone.detected

```json
{
  "event": "phone.detected",
  "event_type": "phone.detected",
  "phone": "+34612345678",
  "detected_at": "2025-01-15T14:30:00Z",
  "shop_id": 123,
  "conversation_hash": "abc123def456..."
}
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `event_type` | string | Siempre "phone.detected" |
| `phone` | string | Numero de telefono detectado (formato internacional) |
| `detected_at` | string | Fecha/hora de deteccion en ISO 8601 UTC |
| `shop_id` | number | ID interno de la tienda en WAzion |
| `conversation_hash` | string | Hash unico de la conversacion |

#### Evento: followup.detected

```json
{
  "event": "followup.detected",
  "event_type": "followup.detected",
  "shop_id": 123,
  "phone": "+34612345678",
  "intent_level": "high",
  "product_mentioned": "Funda iPhone 15 negro",
  "customer_objection": "Pregunto por el precio de envio",
  "outcome": "abandoned",
  "suggested_message": "Hola, vi que te interesaba la funda...",
  "shopify_order_found": false,
  "crm_order_found": false,
  "attempt_number": 1,
  "timestamp": "2026-02-14T10:00:00Z"
}
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `event_type` | string | Siempre "followup.detected" |
| `shop_id` | number | ID interno de la tienda en WAzion |
| `phone` | string | Numero de telefono del cliente (formato internacional) |
| `intent_level` | string | Nivel de intencion detectado: "medium" o "high" |
| `product_mentioned` | string\|null | Producto mencionado en la conversacion |
| `customer_objection` | string\|null | Objecion o duda del cliente |
| `outcome` | string | Estado: "abandoned", "unclear", "converted", "support_only" |
| `suggested_message` | string | Mensaje de seguimiento generado por la IA |
| `shopify_order_found` | boolean | Si se encontro un pedido en tu plataforma e-commerce (Shopify, WooCommerce, PrestaShop, VTEX) |
| `crm_order_found` | boolean | Si se encontro un pedido via CRM endpoint |
| `attempt_number` | number | Numero de intento de seguimiento (1-3) |
| `timestamp` | string | Fecha/hora en ISO 8601 UTC |

#### Evento: followup.replied

Se dispara cuando un cliente responde al mensaje de seguimiento enviado por el Seguimiento Inteligente.

```json
{
  "event": "followup.replied",
  "event_type": "followup.replied",
  "phone": "+34612345678",
  "intent_level": "high",
  "product_mentioned": "Funda iPhone 15 negro",
  "attempt_number": 1,
  "followup_message": "Hola, vi que te interesaba la funda...",
  "days_to_reply": 2.5
}
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `event_type` | string | Siempre "followup.replied" |
| `phone` | string | Numero de telefono del cliente (formato internacional) |
| `intent_level` | string | Nivel de intencion detectado: "medium" o "high" |
| `product_mentioned` | string\|null | Producto mencionado en la conversacion |
| `attempt_number` | number | Numero de intento de seguimiento (1-3) |
| `followup_message` | string | Mensaje de seguimiento que se envio al cliente |
| `days_to_reply` | number | Dias transcurridos desde el seguimiento hasta la respuesta |

#### Evento: followup.converted

Se dispara cuando un cliente realiza una compra despues de recibir un mensaje de seguimiento. La compra se detecta via Shopify, WooCommerce, PrestaShop, VTEX o un endpoint CRM de verificacion.

```json
{
  "event": "followup.converted",
  "event_type": "followup.converted",
  "phone": "+34612345678",
  "intent_level": "high",
  "product_mentioned": "Funda iPhone 15 negro",
  "attempt_number": 1,
  "followup_message": "Hola, vi que te interesaba la funda...",
  "days_to_reply": 3.0
}
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `event_type` | string | Siempre "followup.converted" |
| `phone` | string | Numero de telefono del cliente (formato internacional) |
| `intent_level` | string | Nivel de intencion detectado: "medium" o "high" |
| `product_mentioned` | string\|null | Producto mencionado en la conversacion |
| `attempt_number` | number | Numero de intento de seguimiento (1-3) |
| `followup_message` | string | Mensaje de seguimiento que se envio al cliente |
| `days_to_reply` | number | Dias transcurridos desde el seguimiento hasta la compra |

#### Evento: plugin_chat.session_closed

Se dispara cuando una sesion del widget de chat web finaliza. Incluye un resumen generado por IA, el historial completo de mensajes y la puntuacion de satisfaccion del cliente.

```json
{
  "event": "plugin_chat.session_closed",
  "event_type": "plugin_chat.session_closed",
  "shop_id": 123,
  "session_id": "chat_abc123",
  "ai_summary": "Customer asked about shipping times and return policy. Satisfied with the answers provided.",
  "messages": [
    { "role": "user", "content": "How long does shipping take?", "timestamp": "2026-01-15T14:30:00Z" },
    { "role": "assistant", "content": "Shipping usually takes 3-5 business days.", "timestamp": "2026-01-15T14:30:05Z" }
  ],
  "satisfaction_score": 4.5,
  "closed_at": "2026-01-15T14:35:00Z"
}
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `event_type` | string | Siempre "plugin_chat.session_closed" |
| `shop_id` | number | ID interno de la tienda en WAzion |
| `session_id` | string | Identificador unico de la sesion de chat |
| `ai_summary` | string | Resumen de la conversacion generado por IA |
| `messages` | array | Historial completo de mensajes (cada uno con role, content, timestamp) |
| `satisfaction_score` | number | Puntuacion de satisfaccion del cliente (1-5) |
| `closed_at` | string | Fecha/hora de cierre de la sesion en ISO 8601 UTC |

#### Evento: test

```json
{
  "event_type": "test",
  "message": "This is a test webhook from WAzion",
  "timestamp": "2025-01-15T14:30:00+01:00",
  "shop_id": 123,
  "test": true
}
```

### Headers HTTP Enviados

| Header | Descripcion |
|--------|-------------|
| `Content-Type` | `application/json` |
| `User-Agent` | `WAzion-Webhooks/1.0` |
| `X-Webhook-ID` | ID unico del webhook (formato `wh_XXXXXXXX`, ej: `wh_00012345`) |
| `X-Webhook-Event` | Tipo de evento (ej: `phone.detected`) |
| `X-Webhook-Attempt` | Numero de intento (1-5) |
| `X-Webhook-Timestamp` | Unix timestamp del envio |
| `X-Webhook-Signature` | Firma HMAC-SHA256 (si hay secret configurado) |

### Verificacion de Firma HMAC

Si configuras un `webhook_secret`, WAzion firma cada peticion para que puedas verificar su autenticidad.

**Algoritmo de firma:**
```
signature = HMAC-SHA256(timestamp + "." + payload_json, webhook_secret)
```

**PHP - Verificar firma:**

```php
<?php
function verificarFirmaWebhook($payload, $secret, $signatureRecibida, $timestamp) {
    // Verificar que el timestamp no sea muy antiguo (prevenir replay attacks)
    $currentTime = time();
    if (abs($currentTime - $timestamp) > 300) { // 5 minutos de tolerancia
        return false;
    }

    // Calcular firma esperada
    $signaturePayload = $timestamp . '.' . $payload;
    $signatureEsperada = hash_hmac('sha256', $signaturePayload, $secret);

    // Comparacion segura (timing-safe)
    return hash_equals($signatureEsperada, $signatureRecibida);
}

// Uso:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = (int)($_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? 0);
$secret = 'whsec_tu_secret_aqui';

if (!verificarFirmaWebhook($payload, $secret, $signature, $timestamp)) {
    http_response_code(401);
    exit('Invalid signature');
}
```

**Node.js - Verificar firma:**

```javascript
const crypto = require('crypto');

function verificarFirmaWebhook(payload, secret, signatureRecibida, timestamp) {
    // Verificar timestamp
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - timestamp) > 300) {
        return false;
    }

    // Calcular firma esperada
    const signaturePayload = `${timestamp}.${payload}`;
    const signatureEsperada = crypto
        .createHmac('sha256', secret)
        .update(signaturePayload)
        .digest('hex');

    // Comparacion segura
    return crypto.timingSafeEqual(
        Buffer.from(signatureEsperada),
        Buffer.from(signatureRecibida)
    );
}

// Uso en Express:
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
    const payload = req.body.toString();
    const signature = req.headers['x-webhook-signature'] || '';
    const timestamp = parseInt(req.headers['x-webhook-timestamp'] || '0');
    const secret = 'whsec_tu_secret_aqui';

    if (!verificarFirmaWebhook(payload, secret, signature, timestamp)) {
        return res.status(401).send('Firma invalida');
    }

    // Procesar webhook...
    res.status(200).json({ status: 'ok' });
});
```

**Python - Verificar firma:**

```python
import hmac
import hashlib
import time

def verificar_firma_webhook(payload: str, secret: str, signature_recibida: str, timestamp: int) -> bool:
    # Verificar timestamp
    current_time = int(time.time())
    if abs(current_time - timestamp) > 300:
        return False

    # Calcular firma esperada
    signature_payload = f"{timestamp}.{payload}"
    signature_esperada = hmac.new(
        secret.encode(),
        signature_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Comparacion segura
    return hmac.compare_digest(signature_esperada, signature_recibida)

# Uso en Flask:
@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-Webhook-Signature', '')
    timestamp = int(request.headers.get('X-Webhook-Timestamp', 0))
    secret = 'whsec_tu_secret_aqui'

    if not verificar_firma_webhook(payload, secret, signature, timestamp):
        return 'Firma invalida', 401

    # Procesar webhook...
    return jsonify({'status': 'ok'}), 200
```

### Implementacion del Receptor

#### PHP - Receptor Completo

```php
<?php
/**
 * Receptor de Webhooks WAzion
 * Archivo: /webhooks/wazion.php
 */

// Configuracion
$WEBHOOK_SECRET = 'whsec_tu_secret_aqui'; // Dejar vacio si no usas firma
$LOG_FILE = __DIR__ . '/wazion_webhooks.log';

// Headers de respuesta
header('Content-Type: application/json');

// Solo aceptar POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method not allowed']);
    exit;
}

// Leer payload
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);

// Verificar firma si hay secret configurado
if (!empty($WEBHOOK_SECRET)) {
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    $timestamp = (int)($_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? 0);

    $signaturePayload = $timestamp . '.' . $payload;
    $expectedSignature = hash_hmac('sha256', $signaturePayload, $WEBHOOK_SECRET);

    if (!hash_equals($expectedSignature, $signature)) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid signature']);
        logWebhook('ERROR', 'Firma invalida', $data);
        exit;
    }

    // Verificar timestamp (prevenir replay attacks)
    if (abs(time() - $timestamp) > 300) {
        http_response_code(401);
        echo json_encode(['error' => 'Timestamp expired']);
        logWebhook('ERROR', 'Timestamp expirado', $data);
        exit;
    }
}

// Log del webhook recibido
logWebhook('INFO', 'Webhook recibido', $data);

// Procesar segun tipo de evento
$eventType = $data['event_type'] ?? '';

switch ($eventType) {
    case 'phone.detected':
        procesarNuevoTelefono($data);
        break;

    case 'followup.detected':
        // Seguimiento Inteligente: cliente con intencion detectada
        $phone = $data['phone'] ?? '';
        $intentLevel = $data['intent_level'] ?? ''; // "high" o "medium"
        $product = $data['product_mentioned'] ?? null;
        $suggestedMsg = $data['suggested_message'] ?? null;
        // Crear tarea en tu CRM, notificar al equipo, etc.
        logWebhook('INFO', "Follow-up $intentLevel: $phone", $data);
        break;

    case 'followup.replied':
        // El cliente respondio al mensaje de seguimiento
        logWebhook('INFO', "Cliente {$data['phone']} respondio al seguimiento (dia {$data['days_to_reply']})", $data);
        break;

    case 'followup.converted':
        // El cliente compro tras recibir el seguimiento
        logWebhook('INFO', "Conversion: {$data['phone']} compro tras seguimiento (dia {$data['days_to_reply']})", $data);
        break;

    case 'test':
        // Webhook de prueba - solo confirmar recepcion
        logWebhook('INFO', 'Test webhook recibido correctamente', $data);
        break;

    default:
        logWebhook('WARNING', 'Evento desconocido', $data);
}

// Responder OK
http_response_code(200);
echo json_encode(['status' => 'ok', 'received_at' => date('c')]);

// ============================================
// FUNCIONES
// ============================================

function procesarNuevoTelefono($data) {
    $phone = $data['phone'] ?? '';
    $detectedAt = $data['detected_at'] ?? '';
    $conversationHash = $data['conversation_hash'] ?? '';

    if (empty($phone)) {
        logWebhook('ERROR', 'Telefono vacio en phone.detected', $data);
        return;
    }

    // LOGICA DE TU NEGOCIO:
    // Aqui puedes implementar lo que necesites:

    // 1. Guardar en base de datos
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=tu_db', 'usuario', 'password');
        $stmt = $pdo->prepare("
            INSERT INTO leads_whatsapp (telefono, detectado_at, hash_conversacion, created_at)
            VALUES (?, ?, ?, NOW())
            ON DUPLICATE KEY UPDATE ultima_deteccion = NOW()
        ");
        $stmt->execute([$phone, $detectedAt, $conversationHash]);
        logWebhook('INFO', "Telefono guardado en BD: $phone", $data);
    } catch (Exception $e) {
        logWebhook('ERROR', 'Error guardando en BD: ' . $e->getMessage(), $data);
    }

    // 2. Enviar notificacion por email
    /*
    mail(
        'ventas@tuempresa.com',
        'Nuevo lead WhatsApp: ' . $phone,
        "Se ha detectado un nuevo contacto de WhatsApp:\n\nTelefono: $phone\nFecha: $detectedAt",
        'From: wazion@tuempresa.com'
    );
    */

    // 3. Llamar a otro sistema (CRM, etc)
    /*
    $ch = curl_init('https://tu-crm.com/api/leads');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        'phone' => $phone,
        'source' => 'whatsapp',
        'detected_at' => $detectedAt
    ]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_exec($ch);
    curl_close($ch);
    */

    // 4. Enviar a Slack/Teams
    /*
    $slackWebhook = 'https://hooks.slack.com/services/XXX/YYY/ZZZ';
    $slackMessage = [
        'text' => "Nuevo contacto WhatsApp detectado: $phone"
    ];
    $ch = curl_init($slackWebhook);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($slackMessage));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_exec($ch);
    curl_close($ch);
    */
}

function logWebhook($level, $message, $data = null) {
    global $LOG_FILE;

    $logEntry = [
        'timestamp' => date('Y-m-d H:i:s'),
        'level' => $level,
        'message' => $message,
        'data' => $data
    ];

    file_put_contents(
        $LOG_FILE,
        json_encode($logEntry, JSON_UNESCAPED_UNICODE) . "\n",
        FILE_APPEND
    );
}
```

#### Node.js - Receptor Completo

```javascript
/**
 * Receptor de Webhooks WAzion
 * Archivo: /webhooks/wazion.js (Express.js)
 */

const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const app = express();

// Configuracion
const WEBHOOK_SECRET = 'whsec_tu_secret_aqui'; // Dejar vacio si no usas firma
const LOG_FILE = path.join(__dirname, 'wazion_webhooks.log');

// Middleware para obtener raw body
app.use('/webhook', express.raw({ type: 'application/json' }));

// Funcion de logging
function logWebhook(level, message, data = null) {
    const logEntry = {
        timestamp: new Date().toISOString(),
        level,
        message,
        data
    };
    fs.appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n');
    console.log(`[${level}] ${message}`);
}

// Funcion para verificar firma
function verificarFirma(payload, signature, timestamp) {
    if (!WEBHOOK_SECRET) return true;

    // Verificar timestamp
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - timestamp) > 300) {
        return false;
    }

    // Calcular firma esperada
    const signaturePayload = `${timestamp}.${payload}`;
    const expectedSignature = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(signaturePayload)
        .digest('hex');

    try {
        return crypto.timingSafeEqual(
            Buffer.from(expectedSignature),
            Buffer.from(signature)
        );
    } catch {
        return false;
    }
}

// Procesar nuevo telefono
async function procesarNuevoTelefono(data) {
    const { phone, detected_at, conversation_hash } = data;

    if (!phone) {
        logWebhook('ERROR', 'Telefono vacio en phone.detected', data);
        return;
    }

    // LOGICA DE TU NEGOCIO:

    // 1. Guardar en base de datos
    // const db = require('./db');
    // await db.query('INSERT INTO leads_whatsapp ...', [phone, detected_at]);

    // 2. Enviar notificacion
    // await sendEmail('ventas@tuempresa.com', 'Nuevo lead WhatsApp', ...);

    // 3. Llamar a otro sistema
    // await fetch('https://tu-crm.com/api/leads', { method: 'POST', body: JSON.stringify({phone}) });

    logWebhook('INFO', `Nuevo telefono procesado: ${phone}`, data);
}

// Endpoint del webhook
app.post('/webhook', async (req, res) => {
    try {
        const payload = req.body.toString();
        const signature = req.headers['x-webhook-signature'] || '';
        const timestamp = parseInt(req.headers['x-webhook-timestamp'] || '0');

        // Verificar firma
        if (!verificarFirma(payload, signature, timestamp)) {
            logWebhook('ERROR', 'Firma invalida');
            return res.status(401).json({ error: 'Invalid signature' });
        }

        const data = JSON.parse(payload);
        logWebhook('INFO', 'Webhook recibido', data);

        // Procesar segun tipo de evento
        switch (data.event_type) {
            case 'phone.detected':
                await procesarNuevoTelefono(data);
                break;
            case 'followup.detected':
                // Seguimiento Inteligente: cliente con intencion detectada
                logWebhook('INFO', `Follow-up ${data.intent_level}: ${data.phone}`);
                if (data.product_mentioned) logWebhook('INFO', `Producto: ${data.product_mentioned}`);
                // Crear tarea en tu CRM, notificar al equipo, etc.
                break;
            case 'followup.replied':
                // El cliente respondio al mensaje de seguimiento
                logWebhook('INFO', `Cliente ${data.phone} respondio al seguimiento (dia ${data.days_to_reply})`);
                break;
            case 'followup.converted':
                // El cliente compro tras recibir el seguimiento
                logWebhook('INFO', `Conversion: ${data.phone} compro tras seguimiento (dia ${data.days_to_reply})`);
                break;
            case 'test':
                logWebhook('INFO', 'Test webhook recibido correctamente');
                break;
            default:
                logWebhook('WARNING', `Evento desconocido: ${data.event_type}`, data);
        }

        res.status(200).json({ status: 'ok', received_at: new Date().toISOString() });

    } catch (error) {
        logWebhook('ERROR', `Error procesando webhook: ${error.message}`);
        res.status(500).json({ error: 'Internal server error' });
    }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Webhook receiver listening on port ${PORT}`);
});
```

---

## Codigos de Ejemplo Completos

### Configuracion JSON Completa de Ejemplo

Este es un ejemplo completo de configuracion que incluye funciones personalizadas y CRM endpoints:

**Funciones Personalizadas IA (prompt_custom_functions):**

```json
[
  {
    "name": "consultarDisponibilidadRestaurante",
    "description": "Consulta los horarios disponibles para reservar mesa en el restaurante para una fecha y numero de personas especificos",
    "parameters": {
      "type": "object",
      "properties": {
        "fecha": {
          "type": "string",
          "description": "Fecha para la reserva en formato YYYY-MM-DD (ej: 2025-01-20)"
        },
        "num_personas": {
          "type": "number",
          "description": "Numero de personas para la reserva (1-20)"
        },
        "turno": {
          "type": "string",
          "description": "Turno preferido: 'almuerzo' o 'cena'. Si no se especifica, se muestran ambos"
        },
        "endpoint": {
          "url": "https://api.mirestaurante.com/v1/disponibilidad",
          "method": "GET",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer rk_live_abc123xyz789"
            }
          }
        }
      },
      "required": ["fecha", "num_personas"]
    }
  },
  {
    "name": "crearReserva",
    "description": "Crea una reserva confirmada en el restaurante. Solo usar cuando el cliente haya confirmado todos los datos",
    "parameters": {
      "type": "object",
      "properties": {
        "nombre": {
          "type": "string",
          "description": "Nombre completo del cliente"
        },
        "telefono": {
          "type": "string",
          "description": "Telefono del cliente (con codigo de pais)"
        },
        "email": {
          "type": "string",
          "description": "Email del cliente (opcional)"
        },
        "fecha": {
          "type": "string",
          "description": "Fecha de la reserva (YYYY-MM-DD)"
        },
        "hora": {
          "type": "string",
          "description": "Hora de la reserva (HH:MM)"
        },
        "num_personas": {
          "type": "number",
          "description": "Numero de comensales"
        },
        "notas": {
          "type": "string",
          "description": "Notas especiales: alergias, celebraciones, preferencias de mesa, etc"
        },
        "endpoint": {
          "url": "https://api.mirestaurante.com/v1/reservas",
          "method": "POST",
          "format": "json",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer rk_live_abc123xyz789"
            }
          }
        }
      },
      "required": ["nombre", "telefono", "fecha", "hora", "num_personas"]
    }
  },
  {
    "name": "consultarEstadoPedido",
    "description": "Consulta el estado de un pedido a domicilio usando el numero de pedido",
    "parameters": {
      "type": "object",
      "properties": {
        "numero_pedido": {
          "type": "string",
          "description": "Numero de referencia del pedido (ej: PED-2025-001234)"
        },
        "endpoint": {
          "url": "https://api.mirestaurante.com/v1/pedidos/estado",
          "method": "GET",
          "auth": {
            "type": "query",
            "fields": {
              "api_key": "pk_live_pedidos_xyz"
            }
          }
        }
      },
      "required": ["numero_pedido"]
    }
  },
  {
    "name": "consultarMenu",
    "description": "Obtiene el menu actual del restaurante con precios y disponibilidad",
    "parameters": {
      "type": "object",
      "properties": {
        "categoria": {
          "type": "string",
          "description": "Categoria del menu: 'entrantes', 'principales', 'postres', 'bebidas', 'todo'"
        },
        "solo_disponibles": {
          "type": "boolean",
          "description": "Si es true, solo muestra platos disponibles actualmente"
        },
        "endpoint": {
          "url": "https://api.mirestaurante.com/v1/menu",
          "method": "GET",
          "auth": {
            "type": "header",
            "fields": {
              "Authorization": "Bearer rk_live_abc123xyz789"
            }
          }
        }
      },
      "required": []
    }
  }
]
```

**CRM Endpoints Personalizados (custom_crm_endpoints):**

```json
[
  {
    "type": "sidePanel_CustomerInfo",
    "url": "https://api.mirestaurante.com/v1/wazion/cliente",
    "method": "GET",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer rk_live_abc123xyz789"
      }
    }
  },
  {
    "type": "ai_CustomerInitialInfo",
    "url": "https://api.mirestaurante.com/v1/wazion/contexto",
    "method": "GET",
    "auth": {
      "type": "header",
      "fields": {
        "Authorization": "Bearer rk_live_abc123xyz789"
      }
    }
  },
  {
    "type": "sidePanel_CustomerFindToJoin",
    "url": "https://api.mirestaurante.com/v1/wazion/buscar-clientes",
    "method": "POST",
    "auth": {
      "type": "body",
      "fields": {
        "api_secret": "secret_busqueda_xyz"
      }
    }
  }
]
```

---

## Servidor MCP (Model Context Protocol)

WAzion expone todas las acciones del dashboard como un servidor MCP compatible con el estandar Streamable HTTP (spec 2025-03-26).

### Endpoint

```
POST https://www.wazion.com/api/mcp/
```

### Autenticacion

```
Authorization: Bearer {token_ext}
```

El `token_ext` es el mismo token que aparece en la URL del dashboard (`/dashboard/{token_ext}`).

### Protocolo

JSON-RPC 2.0 sobre HTTP POST. El servidor responde con `Content-Type: application/json`.

### Metodos disponibles

| Metodo | Auth | Descripcion |
|--------|------|-------------|
| `initialize` | No | Handshake del protocolo MCP |
| `notifications/initialized` | No | ACK del cliente (no-op) |
| `tools/list` | Si | Lista todos los tools disponibles |
| `tools/call` | Si | Ejecuta un tool |
| `ping` | Si | Health check |

### Ejemplo: initialize

```json
// Request
{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {},
    "clientInfo": {"name": "mi-app", "version": "1.0"}
  },
  "id": 1
}

// Response
{
  "jsonrpc": "2.0",
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {"tools": {"listChanged": false}},
    "serverInfo": {"name": "wazion-mcp", "version": "1.0.1"}
  },
  "id": 1
}
```

### Ejemplo: tools/call

```json
// Request
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "get_shop_status",
    "arguments": {}
  },
  "id": 2
}

// Response
{
  "jsonrpc": "2.0",
  "result": {
    "content": [{"type": "text", "text": "Estado actual de la tienda\n\n{...}"}]
  },
  "id": 2
}
```

### Acciones con confirmacion

Las acciones peligrosas (eliminar agentes, desconectar tu tienda, etc.) requieren dos llamadas:

1. **Sin `confirm`** → Devuelve un mensaje de advertencia (no ejecuta la accion)
2. **Con `"confirm": true`** → Ejecuta la accion

```json
// Primera llamada: solicitar confirmacion
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "delete_agent",
    "arguments": {"id": 5}
  },
  "id": 3
}
// Response: warning con descripcion de la accion

// Segunda llamada: confirmar
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "delete_agent",
    "arguments": {"id": 5, "confirm": true}
  },
  "id": 4
}
// Response: resultado de la eliminacion
```

### Compatibilidad

Claude Desktop, Claude Code, ChatGPT, Cursor, VS Code Copilot, Windsurf, Cline, y cualquier cliente MCP compatible con Streamable HTTP.

### Configuracion por cliente

**Claude Desktop** (macOS con Homebrew):

Prerequisito: `/opt/homebrew/bin/npm install -g mcp-remote`

Editar `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
  "mcpServers": {
    "wazion": {
      "command": "/opt/homebrew/bin/node",
      "args": [
        "/opt/homebrew/lib/node_modules/mcp-remote/dist/proxy.js",
        "https://www.wazion.com/api/mcp/",
        "--transport", "http-only",
        "--header", "Authorization: Bearer TU_TOKEN"
      ]
    }
  }
}
```

> **Nota**: Claude Desktop usa el Node.js del sistema que puede ser antiguo. Se referencia directamente el binario de Homebrew.

**Claude Code** (`.mcp.json`):
```json
{"mcpServers": {"wazion": {"type": "url", "url": "https://www.wazion.com/api/mcp/", "headers": {"Authorization": "Bearer TU_TOKEN"}}}}
```

**Cursor** (`.cursor/mcp.json`):
```json
{"mcpServers": {"wazion": {"url": "https://www.wazion.com/api/mcp/", "headers": {"Authorization": "Bearer TU_TOKEN"}}}}
```

**VS Code Copilot** (`.vscode/mcp.json`):
```json
{"servers": {"wazion": {"type": "http", "url": "https://www.wazion.com/api/mcp/", "headers": {"Authorization": "Bearer TU_TOKEN"}}}}
```

---

## API de Estado de Mensajes WhatsApp (Message Status)

Consulta el estado de entrega/lectura (`sent` / `delivered` / `read`) de los mensajes de WhatsApp que tu tienda ha enviado. Util para telemetria de campanas (resenas, recuperacion de carritos, marketing): saber cuantos mensajes llegaron al dispositivo y cuantos fueron leidos.

Es un endpoint de **solo lectura**: no envia ni modifica mensajes. El estado se actualiza automaticamente con los acuses de WhatsApp (doble check gris = entregado, doble check azul = leido).

### Endpoint

```
POST https://www.wazion.com/api/whatsapp/message_status.php
```

### Autenticacion

Cabecera con el **webhook secret** de tu tienda (el mismo `whsec_...` de la seccion de Alertas/Webhooks):

```
Authorization: Bearer whsec_TU_SECRET
```

Alternativamente `X-Webhook-Secret: whsec_TU_SECRET`. El endpoint solo devuelve mensajes de tu propia tienda.

### Peticion

```json
{
  "message_ids": ["3EB0AAA...", "3EB0BBB...", "..."]
}
```

- `message_ids`: lista de IDs de mensaje de WhatsApp (los que recibes al enviar). Maximo 500 por llamada.

### Respuesta

```json
{
  "success": true,
  "statuses": {
    "3EB0AAA...": "read",
    "3EB0BBB...": "delivered",
    "3EB0CCC...": "sent"
  }
}
```

- Estados: `sent` (enviado), `delivered` (entregado al dispositivo), `read` (leido). El estado solo avanza, nunca retrocede.
- Los `message_ids` que no existan o no sean de tu tienda no aparecen en `statuses`.

### Ejemplo (cURL)

```bash
curl -X POST https://www.wazion.com/api/whatsapp/message_status.php \
  -H "Authorization: Bearer whsec_TU_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"message_ids":["3EB0AAA...","3EB0BBB..."]}'
```

### Codigos de respuesta

| Codigo | Significado |
|--------|-------------|
| 200 | OK (devuelve `statuses`) |
| 401 | Falta el secret o no es valido |
| 405 | Metodo no permitido (usar POST) |

> **Nota**: el estado se registra a partir del momento del envio. Mensajes enviados antes de activar esta funcion pueden no tener estado de entrega/lectura.

---

## Errores Comunes y Solucion

### Funciones Personalizadas IA

| Error | Causa | Solucion |
|-------|-------|----------|
| "JSON invalido" | Sintaxis JSON incorrecta | Usar un validador JSON online, verificar comillas y comas |
| "Falta campo endpoint" | No hay objeto `endpoint` en `properties` | Agregar `endpoint` dentro de `parameters.properties` |
| "Falta endpoint.url" | URL no especificada | Agregar `"url": "https://..."` dentro de endpoint |
| "URL no valida" | Formato de URL incorrecto | Usar URL completa con `https://` |
| "URL blocked" / "URL bloqueada por seguridad" | La URL apunta a una IP privada o reservada | Usar una URL publica. No se permiten IPs privadas (10.x, 192.168.x, 127.x), localhost, ni rangos reservados |
| "Could not resolve hostname" | El dominio no se puede resolver | Verificar que el dominio existe y tiene registros DNS validos |
| "Error de conexion" | Tu servidor no responde | Verificar que la URL sea accesible publicamente |
| "Timeout" | Tu API tarda mucho | Optimizar tu endpoint para responder en <5 segundos |

### CRM Endpoints

| Error | Causa | Solucion |
|-------|-------|----------|
| "Tipo no valido" | `type` incorrecto | Usar: `sidePanel_CustomerInfo`, `ai_CustomerInitialInfo`, `sidePanel_CustomerFindToJoin`, `search_Products` |
| "CRM endpoint URL blocked" | La URL apunta a una IP privada o reservada | Usar una URL publica. No se permiten IPs privadas, localhost, ni rangos reservados |
| "Falta campo found" | Respuesta sin `found` | Tu API debe devolver `{"found": true/false, ...}` |
| "No es JSON valido" | Tu API no devuelve JSON | Configurar `Content-Type: application/json` en tu respuesta |

### Alertas Automaticas

| Error | Causa | Solucion |
|-------|-------|----------|
| "URL debe ser HTTPS" | URL con HTTP | Usar HTTPS (excepto localhost para pruebas) |
| "Firma invalida" | Secret incorrecto o mal calculada | Verificar el algoritmo HMAC-SHA256 |
| "Codigo HTTP 4xx/5xx" | Error en tu servidor | Revisar logs de tu servidor |

---

## Checklist de Implementacion

### Funciones Personalizadas IA

- [ ] Crear endpoint en tu servidor (HTTPS)
- [ ] Implementar autenticacion
- [ ] Validar parametros de entrada
- [ ] Devolver JSON valido
- [ ] Responder en menos de 5 segundos
- [ ] Manejar errores gracefully
- [ ] Crear JSON de configuracion
- [ ] Probar en WAzion Dashboard
- [ ] Verificar logs de errores

### CRM Endpoints

- [ ] Implementar `sidePanel_CustomerInfo`
  - [ ] Recibir `phone`, `token`, `relatedphones`
  - [ ] Devolver `found`, `customer`, `orders`
- [ ] Implementar `ai_CustomerInitialInfo`
  - [ ] Devolver `found`, `info` (texto descriptivo)
- [ ] Implementar `sidePanel_CustomerFindToJoin` (opcional)
  - [ ] Recibir `query`
  - [ ] Devolver `results` con id, name, email, phone
- [ ] Implementar `search_Products` (opcional)
  - [ ] Recibir `q`
  - [ ] Devolver `products`
- [ ] Crear JSON de configuracion
- [ ] Probar con "Probar conexion CRM"
- [ ] Verificar visualizacion en panel lateral

### Alertas Automaticas

- [ ] Crear endpoint receptor (HTTPS)
- [ ] Implementar verificacion de firma (opcional pero recomendado)
- [ ] Manejar evento `phone.detected`
- [ ] Responder con codigo HTTP 2xx
- [ ] Implementar logging
- [ ] Configurar URL en WAzion
- [ ] Configurar Secret (opcional)
- [ ] Activar eventos deseados
- [ ] Probar con "Enviar alerta de prueba"
- [ ] Verificar recepcion en tu sistema

---

## Soporte

Si tienes dudas sobre la implementacion:

1. Revisa los logs de error en tu servidor
2. Usa el boton "Probar conexion" en WAzion Dashboard
3. Verifica que tus endpoints sean accesibles publicamente
4. Contacta al soporte de WAzion en support@wazion.com

---

**Documento generado para WAzion v2.0**
**Ultima actualizacion: Diciembre 2025**
