Sugerencias con IA en tickets
Rellena el formulario de un ticket con IA a partir de texto, audio o imágenes del usuario, y registra qué hizo con las sugerencias. Permiso support:tickets:ai.
Actualizado el . 9 min de lectura
Incluido en los planes Profesional y Avanzado. Ver precios
Con estos endpoints la IA propone el título, la descripción, la
categoría y la prioridad de un ticket a partir del texto, el audio o
las imágenes que aporta el usuario. Funciona en segundo plano: envías
el contenido, recibes un identificador de trabajo (jobId) y lees el
resultado por SSE o consultando su estado cada poco. Todas las
peticiones llevan la cabecera X-Api-Key.
Plan y permisos
Hace falta el aplicativo Tickets de soporte con la capacidad de
tickets propios (support.own_tickets) en el plan de tu organización,
y el permiso support:tickets:ai en la aplicación.
| Endpoint | Permiso de la API (scope) |
|---|---|
POST /external/v1/tickets/ai-suggest | support:tickets:ai |
GET /external/v1/tickets/ai-suggest/:jobId/stream | support:tickets:ai |
GET /external/v1/tickets/ai-suggest/:jobId | support:tickets:ai |
POST /external/v1/tickets/ai-track-action | support:tickets:ai |
POST /external/v1/tickets/ai-transcribe | support:tickets:ai |
POST /external/v1/tickets/ai-transcribe-async | support:tickets:ai |
support:tickets:ai es un permiso sensible: consume cuota de IA y
envía datos del usuario a un proveedor externo. Solo lo concede un
Super Administrador, con un motivo de al menos 20 caracteres. Se exige
desde la primera llamada, sin periodo de gracia: sin él, la respuesta
es 403 INSUFFICIENT_SCOPE. Cómo se conceden los permisos está en
Permisos de la API.
Resumen de endpoints
| Método | Ruta | Descripción |
|---|---|---|
POST | /api/external/v1/tickets/ai-suggest | Pedir sugerencias a partir de texto, audio o imágenes |
GET | /api/external/v1/tickets/ai-suggest/:jobId/stream | Resultado del trabajo por SSE |
GET | /api/external/v1/tickets/ai-suggest/:jobId | Estado del trabajo, consultado cada poco |
POST | /api/external/v1/tickets/ai-track-action | Registrar qué hizo el usuario con las sugerencias |
POST | /api/external/v1/tickets/ai-transcribe | Transcribir un audio en el momento (ver Transcripción) |
POST | /api/external/v1/tickets/ai-transcribe-async | Transcribir un audio en segundo plano |
El SSE no usa tipos de evento: léelo antes de integrar sin el SDK
El stream emite todos los eventos como un único tipo message con el cuerpo en JSON:
data: {"event":"progress","progress":42}
data: {"event":"completed","result":{"suggestions":{...}}}
No hay líneas event: aparte, así que los lectores que enlazan por tipo (EventSource.addEventListener('completed', ...), sseclient con event.event == 'completed') no reciben nunca los eventos y la integración se queda «cargando» para siempre. Lee el tipo en el campo event del JSON; más abajo tienes el ejemplo correcto.
Para autenticar el SSE lo normal es la cabecera X-Api-Key. Como excepción, se acepta también la clave en el parámetro ?apiKey=, solo en peticiones GET con Accept: text/event-stream, que es lo que envía EventSource y lo que usa el SDK oficial. Una clave en la dirección queda en los registros de servidores y proxies, en el historial del navegador y en la cabecera Referer: si puedes, usa una librería que admita cabeceras, pasa la petición por tu servidor o consulta el estado cada poco.
Flujo completo
Tu aplicación envía lo que cuenta el usuario, recoge las sugerencias cuando están listas, rellena el formulario y, al final, avisa de qué hizo el usuario con ellas.
Sugerencias con IA para un ticket
Participan: Usuario, Tu aplicación, UTILIA OS.
De Usuario a Tu aplicación:
Describe el problema
Con texto, una nota de voz, capturas o una combinación.De Tu aplicación a UTILIA OS:
POST /api/external/v1/tickets/ai-suggest
Texto, audio en base64 o imágenes.De UTILIA OS a Tu aplicación:
jobId (respuesta)
Identifica el trabajo.De Tu aplicación a UTILIA OS:
GET /api/external/v1/tickets/ai-suggest/:jobId/stream
O consulta el estado cada poco con GET /ai-suggest/:jobId.Nota en UTILIA OS:
Analiza el contenido con IA y envía eventos progress.De UTILIA OS a Tu aplicación:
Evento completed con las sugerencias (respuesta)
Título, descripción, categoría, prioridad y analyticsEventId.De Tu aplicación a Usuario:
Formulario rellenado
Puede aceptar, cambiar o descartar cada sugerencia.De Tu aplicación a UTILIA OS:
POST /api/external/v1/tickets/ai-track-action
La decisión del usuario, con el analyticsEventId, para medir la calidad.
Pedir sugerencias
/api/external/v1/tickets/ai-suggestEnvía texto, audio o imágenes para que la IA proponga cómo rellenar el formulario del ticket. Permiso: support:tickets:ai.
Encabezados requeridos
- X-Api-Keystringobligatorio
- Clave de API externa (formato: utl_xxx)
- Content-Typestringobligatorio
- application/json
- x-request-idstring
- Identificador único para seguir la petición
Cuerpo de la petición
- userIdstringobligatorio
- Identificador externo del usuario. Tiene que existir antes: si es nuevo, llama primero a /users/identify
- textstring
- Descripción del problema en texto libre
- audioBase64string
- Audio codificado en base64 para transcripción y análisis
- audioFilenamestring
- Nombre del archivo de audio con extensión (por ejemplo, grabacion.webm)
- existingFieldsExternalExistingFieldsDto
- Valores actuales del formulario para contextualizar las sugerencias
- imagesImageBase64Dto[]
- Imágenes para análisis visual. Solo se analizan las 5 primeras (ver aviso más abajo)
- sourcestring
- Origen de la petición: SDK_EXTERNAL (por defecto) o WIDGET
Tienes que enviar al menos uno de estos campos: text, audioBase64 o images. Sin ninguno, la respuesta es 400 con el código AI_NO_INPUT.
Solo se analizan las 5 primeras imágenes, y sin avisar
El tope de 5 imágenes no es una validación: enviar más no da error.
La petición se acepta con 201, pero se descartan sin avisar todas las
imágenes a partir de la sexta, en el orden en que vengan. Si necesitas
analizar más de cinco capturas, repártelas en varias peticiones: si las
mandas todas juntas, las últimas no se analizan y nada en la respuesta
lo indica.
Objeto existingFields (ExternalExistingFieldsDto)
- titlestring
- Título actual del formulario
- descriptionstring
- Descripción actual del formulario
- categorystring
- Categoría seleccionada actualmente
- prioritystring
- Prioridad seleccionada actualmente
- currentUrlstring
- Página en la que el usuario informa del problema
- userAgentstring
- Navegador del usuario
- appVersionstring
- Versión de tu aplicación
Cada elemento de images (ImageBase64Dto)
- base64stringobligatorio
- Contenido de la imagen codificado en base64 (sin el prefijo data:)
- filenamestringobligatorio
- Nombre del archivo con extensión (ejemplo: captura.png)
- mimeTypestringobligatorio
- Tipo MIME de la imagen (ejemplo: image/png, image/jpeg)
Respuesta correcta (201 Created)
- jobIdstring
- Identificador del trabajo. Úsalo para consultar el resultado
Errores
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 400 | AI_NO_INPUT | No se ha enviado texto, audio ni imágenes |
| 404 | USER_NOT_FOUND | No existe ningún usuario con ese identificador externo. Llama primero a /users/identify |
| 401 | -- | Falta la clave de API o no es válida |
Ejemplos
# Ejemplo con texto
curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/ai-suggest' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: utl_tu_clave_de_api' \
-H 'x-request-id: req-ai-suggest-001' \
-d '{
"userId": "usr-4821",
"text": "Cuando intento exportar el informe mensual a PDF, la página se queda cargando indefinidamente y nunca genera el archivo.",
"existingFields": {
"category": "BUG"
},
"source": "SDK_EXTERNAL"
}'Envía en existingFields lo que el usuario ya haya rellenado: la IA lo tiene en cuenta para no repetirlo y proponer lo que falta.
Recibir el resultado por SSE
/api/external/v1/tickets/ai-suggest/:jobId/streamConexión SSE que envía el progreso y el resultado del trabajo. Permiso: support:tickets:ai.
Este endpoint abre una conexión Server-Sent Events (SSE) que envía eventos a medida que avanza el análisis. Si el trabajo ya había terminado al conectar, el servidor envía el resultado al momento y cierra la conexión. El método es GET: con POST, la respuesta es 404 Not Found.
Parámetros de ruta
- jobIdstringobligatorio
- Identificador del trabajo que devolvió POST /ai-suggest
Cabeceras
- X-Api-Keystring
- Clave de API externa (formato: utl_xxx). Obligatoria salvo que la envíes en el parámetro
?apiKey=, admitido solo en estas peticiones SSE. - Acceptstring
- Recomendado: text/event-stream. Imprescindible si te autenticas con
?apiKey=en la dirección.
Formato de la respuesta
El servidor responde con Content-Type: text/event-stream y emite bloques separados por \n\n. Todos los bloques se emiten como un único tipo de mensaje SSE (sin línea event:). El tipo lógico del evento viene dentro del JSON, en el campo event:
data: {"event":"progress","progress":25,"status":"processing"}
data: {"event":"progress","progress":75,"status":"processing"}
data: {"event":"completed","result":{"suggestions":{"title":"Error al exportar informe a PDF","description":"...","category":"BUG","priority":"ALTA"},"transcription":null,"analyticsEventId":"evt_xyz789"}}
Por eso los lectores que enlazan por tipo no funcionan. Por ejemplo, estas formas no reciben nunca los eventos:
// INCORRECTO: el servidor no envía línea `event:`, así que estos listeners nunca se disparan.
es.addEventListener('completed', ...);
es.addEventListener('progress', ...);# INCORRECTO: sseclient enlaza por `event.event`, que aquí siempre es 'message'.
if event.event == 'completed': ...Lo correcto es usar el manejador genérico (onmessage o event.event == 'message') y leer el campo event del JSON, como en los ejemplos de más abajo.
Tipos de eventos (campo event dentro del JSON)
event | Descripción | Resto del JSON |
|---|---|---|
progress | Progreso del análisis | progress: number (0-100), status: string |
completed | El análisis ha terminado bien | result: { suggestions?, transcription?, analyticsEventId? } |
failed | El análisis ha fallado | error: string |
heartbeat | Señal para mantener viva la conexión, cada 30 segundos | (sin más campos) |
Estructura del result cuando event es completed
- result.suggestions.titlestring | null
- Título sugerido para el ticket
- result.suggestions.descriptionstring | null
- Descripción sugerida
- result.suggestions.categorystring | null
- Categoría sugerida: BUG, CONSULTA o SUGERENCIA
- result.suggestions.prioritystring | null
- Prioridad sugerida: BAJA, MEDIA, ALTA o CRITICA
- result.transcriptionstring | null
- Texto transcrito del audio (solo si se envió audio)
- result.analyticsEventIdstring | null
- Identificador que hay que enviar después a /ai-track-action
Ejemplos
curl -N -X GET 'https://os.utilia.ai/api/external/v1/tickets/ai-suggest/job_abc123def456/stream' \
-H 'X-Api-Key: utl_tu_clave_de_api' \
-H 'Accept: text/event-stream' \
-H 'x-request-id: req-ai-stream-001'
# Salida real (sin líneas `event:`, el tipo va dentro del JSON):
# data: {"event":"progress","progress":0,"status":"pending"}
#
# data: {"event":"progress","progress":75,"status":"processing"}
#
# data: {"event":"completed","result":{"suggestions":{"title":"Error al exportar informe a PDF","description":"...","category":"BUG","priority":"ALTA"},"transcription":null,"analyticsEventId":"evt_xyz789"}}Consultar el estado del trabajo
/api/external/v1/tickets/ai-suggest/:jobIdEstado actual de un trabajo de sugerencias, como alternativa al SSE. Permiso: support:tickets:ai.
Es la alternativa al SSE: consultas el estado cada poco hasta que el trabajo termine o falle.
Parámetros de ruta
- jobIdstringobligatorio
- Identificador del trabajo que devolvió POST /ai-suggest
Respuesta correcta (200 OK)
- statusstringobligatorio
- Estado del trabajo: pending, processing, completed, failed o not_found
- progressnumber
- Porcentaje de progreso (0-100). Solo presente en estados pending y processing
- resultExternalAiResultDto
- Resultado del análisis. Solo presente cuando status es completed
- errorstring
- Mensaje de error. Solo presente cuando status es failed
Estructura de result (ExternalAiResultDto)
- suggestionsobject
- Sugerencias generadas: { title?, description?, category?, priority? }
- transcriptionstring | null
- Texto transcrito del audio, si se envió audio
- analyticsEventIdstring | null
- Identificador necesario para registrar la decisión del usuario con /ai-track-action
Ejemplos
curl -X GET 'https://os.utilia.ai/api/external/v1/tickets/ai-suggest/job_abc123def456' \
-H 'X-Api-Key: utl_tu_clave_de_api' \
-H 'x-request-id: req-ai-poll-001'
# Respuesta cuando el job está en progreso:
# {
# "status": "processing",
# "progress": 60
# }
# Respuesta cuando el job ha completado:
# {
# "status": "completed",
# "result": {
# "suggestions": {
# "title": "Error al exportar informe a PDF",
# "description": "Al intentar exportar el informe mensual...",
# "category": "BUG",
# "priority": "ALTA"
# },
# "transcription": null,
# "analyticsEventId": "evt_xyz789"
# }
# }Si prefieres no mantener una conexión SSE abierta, consulta este endpoint una vez por segundo. Para el usuario, el SSE da mejor experiencia.
Registrar la decisión del usuario
/api/external/v1/tickets/ai-track-actionRegistra qué hizo el usuario con las sugerencias, para medir su calidad. Permiso: support:tickets:ai.
Con este endpoint cuentas qué hizo el usuario con las sugerencias: si las aceptó tal cual, las cambió o las descartó. Sirve para medir y mejorar su calidad.
Cabeceras
- X-Api-Keystringobligatorio
- Clave de API externa (formato: utl_xxx)
- Content-Typestringobligatorio
- application/json
- x-request-idstring
- Identificador único para seguir la petición
Cuerpo de la petición
- eventIdstringobligatorio
- El analyticsEventId que vino en el resultado del trabajo
- userActionstringobligatorio
- Lo que hizo el usuario: ACCEPTED_ALL, ACCEPTED_PARTIAL, MODIFIED, REJECTED o IGNORED
- fieldsAcceptedstring[]
- Nombres de los campos que el usuario aceptó sin cambios (ejemplo: ["title", "category"])
- fieldsModifiedstring[]
- Nombres de los campos que el usuario modificó
- fieldsRejectedstring[]
- Nombres de los campos que el usuario descartó
- timeToDecisionMsnumber
- Milisegundos desde que se mostraron las sugerencias hasta la decisión (mínimo 0). Ayuda a valorar su calidad.
- sourcestring
- Origen de la petición: SDK_EXTERNAL o WIDGET
Respuesta correcta (200 OK)
- successboolean
- Indica si el registro se procesó correctamente
Ejemplos
curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/ai-track-action' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: utl_tu_clave_de_api' \
-H 'x-request-id: req-ai-track-001' \
-d '{
"eventId": "evt_xyz789",
"userAction": "MODIFIED",
"fieldsAccepted": ["category", "priority"],
"fieldsModified": ["title", "description"],
"fieldsRejected": [],
"timeToDecisionMs": 4500,
"source": "SDK_EXTERNAL"
}'Ejemplo completo
Este ejemplo junta todos los endpoints de sugerencias en un flujo real:
const API_BASE = 'https://os.utilia.ai/api/external/v1/tickets';
const headers = {
'Content-Type': 'application/json',
'X-Api-Key': 'utl_tu_clave_de_api',
};
// 1. Enviar texto para análisis
const suggestResponse = await fetch(`${API_BASE}/ai-suggest`, {
method: 'POST',
headers,
body: JSON.stringify({
userId: 'usr-4821',
text: 'La página de informes no carga y muestra un error 500 al pulsar Exportar PDF.',
source: 'SDK_EXTERNAL',
}),
});
const { jobId } = await suggestResponse.json();
// 2. Consultar resultado con polling
let result;
while (true) {
const pollResponse = await fetch(`${API_BASE}/ai-suggest/${jobId}`, { headers });
const job = await pollResponse.json();
if (job.status === 'completed') {
result = job.result;
break;
}
if (job.status === 'failed') {
throw new Error(job.error);
}
await new Promise((r) => setTimeout(r, 1000));
}
// 3. Mostrar sugerencias en el formulario
console.log('Título sugerido:', result.suggestions.title);
console.log('Descripción sugerida:', result.suggestions.description);
console.log('Categoría sugerida:', result.suggestions.category);
console.log('Prioridad sugerida:', result.suggestions.priority);
// 4. Registrar la decisión del usuario
const startTime = Date.now();
// ... el usuario revisa y modifica las sugerencias ...
const decisiónTime = Date.now() - startTime;
await fetch(`${API_BASE}/ai-track-action`, {
method: 'POST',
headers,
body: JSON.stringify({
eventId: result.analyticsEventId,
userAction: 'MODIFIED',
fieldsAccepted: ['category', 'priority'],
fieldsModified: ['title', 'description'],
timeToDecisionMs: decisiónTime,
source: 'SDK_EXTERNAL',
}),
});Siguiente paso
Con el formulario relleno, crea el ticket con
POST /external/v1/tickets.
Si el usuario prefiere hablar, la
transcripción de audio
convierte su mensaje de voz en texto. Desde el SDK, lo mismo está en
Sugerencias con IA desde el SDK.