Este es el webservice principal del modo Full Stack. Recibe el mensaje del visitante, orquesta internamente la IA, la búsqueda semántica sobre el inventario del cliente y arma la respuesta lista para mostrar. Guarda el mensaje del visitante y la respuesta de la IA en la conversación (no hace falta llamar a otro endpoint para persistirlos).
Este endpoint consume tokens de IA de la cuenta del cliente (customer_ai_config). Antes de invocarlo podés chequear el saldo disponible con GET /api/action/mapaprop-chat-ai-v1/validate.
| Información del recurso | |
|---|---|
| Autenticación | Requerida (Token de API, Bearer) |
| Scope | mapaprop-chat-ai |
| HTTP Method | POST |
| Response | JSON |
| Version | 1 |
URL del recurso
https://mapaprop.app/api/action/mapaprop-chat-ai-v1/chat
Parámetros
El cuerpo de la petición es un JSON. customerId y websiteId no se envían: se derivan del token de autenticación.
| Key | Type | Required | Descripción |
|---|---|---|---|
| message | string | sí | El mensaje del visitante. |
| questionId | int | no | El ID de la conversación a continuar. Si se omite, se crea una conversación nueva y el questionId asignado viene en la respuesta (campo questionId). Reenvialo en cada turno siguiente. También podés obtenerlo de una llamada previa a POST /init. |
| leadName | string | no | Nombre del visitante. Default Visitante. Si la conversación ya tiene un contacto asociado y no se envía, se conserva el existente. |
| leadEmail | string | no | Email del visitante. Si se envía y la conversación todavía no tiene contacto asociado, se crea o vincula un contacto en el CRM del cliente. |
| leadPhone | string | no | Teléfono del visitante. |
| sessionId | string | no | Identificador de sesión del visitante (para trazabilidad). Si no se envía, se genera un UUID automáticamente. |
| mode | string | no | chat (default), search o semantic. Ver "Modo de búsqueda" más abajo. |
| propertyIds | array de objetos {id: int} | no | IDs de propiedades pre-filtradas (por ejemplo, obtenidas de una búsqueda previa por tu cuenta). El modelo decide si el mensaje es una consulta factual ("¿cuál es más cara?") o semántica ("¿cuál tiene pileta?") sobre este conjunto. |
| useVectorSearch | boolean | no | Fuerza una búsqueda semántica (vectorial) sobre las propiedades del cliente para armar el contexto de la respuesta. Se activa automáticamente si se envía propertyIds o mode=semantic. |
| searchCriteria | object | no | Filtro de búsqueda clásico (operation, type, zone, priceMin, priceMax, currency) usado solo cuando mode=search y no se envió propertyIds. Modo legacy: se recomienda propertyIds o useVectorSearch. |
| propertyActive | object {propertyId: int, ...} | no | Indica que la conversación es sobre UNA propiedad puntual (el visitante está viendo su ficha). Tiene prioridad sobre cualquier otro modo de búsqueda. |
| searchContext | object | no | Contexto del flujo guiado (operación / tipo / zona ya elegidos por el visitante mediante botones). Uso avanzado, reservado para reproducir el flujo del widget oficial. |
| customerSearch | object | no | Datos de búsquedas/favoritos/consultas previas del visitante, usados para enriquecer el contacto creado en el CRM. Uso avanzado. |
| sourceUrl | string | no | URL de la página donde el visitante está conversando. Se guarda en la conversación. |
Modo de búsqueda
- Si enviás
propertyIds, la IA responde en base a ese conjunto (con re-ranking semántico si además mandásuseVectorSearch=true). - Si enviás
useVectorSearch=true(omode=semantic) sinpropertyIds, se busca semánticamente sobre todo el inventario del cliente. - Si enviás
propertyActive, la respuesta se enfoca en esa única propiedad (tiene prioridad sobre todo lo demás). - Sin ninguno de los anteriores, es una conversación libre (
chatsin contexto de búsqueda).
Código de ejemplo
POST /api/action/mapaprop-chat-ai-v1/chat HTTP/1.1
Host: mapaprop.app
Content-Type: application/json
Authorization: Bearer {access_token}
{
"message": "¿Tenés departamentos de 2 ambientes en Palermo?",
"questionId": 481203,
"leadName": "Juan Pérez",
"leadEmail": "juan@example.com",
"sessionId": "b6d1c6b0-2f2a-4e2e-9a4a-8e0a2a2f2a11",
"useVectorSearch": true
}
Respuesta
La respuesta incluye el questionId de la conversación (el asignado si era nueva, o el que enviaste) — guardalo y reenvialo en cada turno siguiente para mantener el hilo — y el turnId de esta respuesta puntual, que usás para dar feedback sobre ella. También devuelve el consumo de tokens del período (totalTokensUsed / tokensRemaining).
| Objeto | Campo | Tipo | Requerido | Descripción |
|---|---|---|---|---|
| Response | response | string | sí | El texto de la respuesta de la IA, listo para mostrar. |
| questionId | int | sí | ID de la conversación (el asignado si era nueva, o el que enviaste). Reenvialo en el siguiente turno. | |
| turnId | long | no | ID de este turno. Usalo para dar feedback sobre esta respuesta con POST /feedback o POST /signal. Puede faltar si el turno no se registró. | |
| action | string | no | Acción sugerida por el modelo. Valores posibles: greeting, show_properties, confirm_search, request_info, contact. Puede venir vacío. | |
| success | boolean | sí | false cuando el turno no pudo procesarse (ver "Errores controlados" más abajo). | |
| propertyIds | array de int | no | IDs de las propiedades referenciadas en esta respuesta (solo presente en modo búsqueda). | |
| buttons | array de objetos | no | Botones sugeridos para el visitante (solo presente en algunos flujos). | |
| allPropertyIds | array de int | no | Todos los IDs del resultado de búsqueda (antes de recortar a los mostrados). Solo modo búsqueda. | |
| hasMoreResults | boolean | sí | true si hay más resultados de búsqueda de los mostrados en propertyIds. | |
| focusedPropertyId | int | no | ID de la propiedad en foco (modo propiedad única). | |
| focusedPropertyCode | string | no | Código de la propiedad en foco. | |
| searchCriteria | object | no | Criterio de búsqueda interpretado por el modelo (modo búsqueda). | |
| tokensUsed | object | sí | Detalle de tokens consumidos en este turno: {input, output, total}. | |
| totalTokensUsed | int | no | Tokens de IA consumidos por la cuenta en el período actual (tras este turno). | |
| tokensRemaining | int | no | Tokens de IA restantes en el período actual. | |
| error | string | no | Presente solo cuando success == false. Ver tabla de abajo. |
Errores controlados (success: false, HTTP 200)
Estos escenarios no lanzan una excepción: la respuesta llega con success: false, un response en español listo para mostrarle al visitante, y un error con el motivo.
| Error | Causa |
|---|---|
TOKEN_LIMIT_EXCEEDED | El cliente alcanzó el límite mensual de tokens de IA de su plan. |
INSUFFICIENT_TOKENS | No quedan tokens suficientes en el período actual para procesar este mensaje puntual. |
CONVERSATION_LIMIT_EXCEEDED | Esta conversación (questionId) ya consumió el máximo de tokens permitido por conversación. Hay que iniciar una conversación nueva. |
USER_MESSAGE_TOO_LONG | El mensaje enviado supera el largo máximo permitido (~300 tokens, ~225 palabras). |
| (mensaje en español, no un código corto) | Falla interna del motor de IA (proveedor caído, error de parseo, etc.). El campo error en este caso contiene el mismo texto que response, no un código corto. |
Errores de validación (se corta la request, no llega success: false)
| Error | Causa |
|---|---|
REQUIRED_INPUT — "Message is required" | Falta el campo message en el body. |
Ejemplo de respuesta
{
"response": "Sí, tenemos 3 departamentos de 2 ambientes en Palermo. Te comparto los más relevantes.",
"questionId": 481203,
"turnId": 481512,
"action": "show_properties",
"success": true,
"propertyIds": [2601234, 2601987, 2602111],
"hasMoreResults": false,
"tokensUsed": {
"input": 812,
"output": 143,
"total": 955
},
"totalTokensUsed": 18420,
"tokensRemaining": 81580
}
Ejemplo de error controlado (límite de tokens del período):
{
"response": "Lo siento, se ha alcanzado el limite de uso mensual del chat. El limite se reiniciara el 2026-09-01.",
"action": "",
"success": false,
"hasMoreResults": false,
"tokensUsed": {
"input": 0,
"output": 0,
"total": 0
},
"error": "TOKEN_LIMIT_EXCEEDED"
}