Tickets de soporte
Abre tickets de soporte en nombre de tus usuarios, añade mensajes con adjuntos, ciérralos o reábrelos y recibe los cambios en tiempo real.
Actualizado el . 5 min de lectura
Incluido en los planes Profesional y Avanzado. Ver precios
El servicio de tickets conecta a las personas de tu aplicación con el equipo de soporte de tu organización en UTILIA OS. Desde tu aplicación abren un ticket, conversan con el equipo y lo cierran; el equipo lo atiende desde Tickets de soporte, necesitas entrar con tu cuenta y, si hace falta, lo convierte en una tarea de Gestión de proyectos. Funciona con la clave de tu aplicación externa, siempre desde tu servidor.
Vida de un ticket entre tu aplicación y el equipo
Participan: Tu aplicación, UTILIA OS, Equipo de soporte.
De Tu aplicación a UTILIA OS:
POST /api/external/v1/tickets
tickets.create abre el ticket en nombre de la persona.De UTILIA OS a Tu aplicación:
id y ticketKey, por ejemplo APP-0001 (respuesta)
De Tu aplicación a UTILIA OS:
GET /api/external/v1/tickets/events
streamUpdates abre la conexión de eventos.De Equipo de soporte a UTILIA OS:
Responde en Tickets de soporte
De UTILIA OS a Tu aplicación:
Evento comment-added
Por la conexión abierta, con ticketId y ticketKey.De Tu aplicación a UTILIA OS:
GET /api/external/v1/tickets/:id
tickets.get trae los mensajes; los del equipo llevan author.type INTERNAL.De Tu aplicación a UTILIA OS:
POST /api/external/v1/tickets/:id/messages
addMessage con la respuesta de la persona.De Tu aplicación a UTILIA OS:
PATCH /api/external/v1/tickets/:id/close
close cuando el problema está resuelto.
Antes de empezar
| JavaScript | Python | Para qué | Scope |
|---|---|---|---|
tickets.create(datos) | tickets.create(datos) | Abrir un ticket | support:tickets:write |
tickets.list(userId, filtros?) | tickets.list(user_id, filtros) | Listar los tickets de un usuario | support:tickets:read |
tickets.get(ticketId, userId) | tickets.get(ticket_id, user_id) | Ver un ticket con sus mensajes | support:tickets:read |
tickets.addMessage(ticketId, userId, datos) | tickets.add_message(ticket_id, user_id, datos) | Añadir un mensaje | support:tickets:write |
tickets.close(ticketId, userId) | tickets.close(ticket_id, user_id) | Cerrar un ticket | support:tickets:write |
tickets.reopen(ticketId, userId) | tickets.reopen(ticket_id, user_id) | Reabrir un ticket cerrado | support:tickets:write |
tickets.getUnreadCount(userId) | tickets.get_unread_count(user_id) | Mensajes sin leer del usuario | support:tickets:read |
tickets.streamUpdates(userId?, opciones?) | tickets.stream_updates(user_id) | Cambios en tiempo real | support:tickets:read |
tickets.createStreamToken(datos) | tickets.create_stream_token(route, ...) | Pase para abrir la conexión en tiempo real por tu cuenta (4.2.0) | support:tickets:read (support:tickets:ai en las conexiones de IA) |
Sin el scope, la respuesta es un 403 con errorCode INSUFFICIENT_SCOPE. Antes se comprueba el plan: hacen falta el aplicativo Tickets de soporte y su capacidad «Tickets propios»; si faltan, el 403 es un bloqueo de plan. En todos los métodos, userId es el externalId con el que identificaste a la persona: el servidor comprueba que el ticket es suyo.
Abrir un ticket
| JavaScript | Python | Obligatorio | Qué es |
|---|---|---|---|
user | user | Sí | Quién lo abre: externalId obligatorio y, si quieres, email, name y metadata. |
title | title | Sí | Título, de 5 a 500 caracteres. |
description | description | Sí | Descripción, 10 caracteres como mínimo. |
category | category | No | BUG (algo no funciona), CONSULTA o SUGERENCIA. Si no la mandas, el servidor pone CONSULTA. |
priority | priority | No | BAJA, MEDIA, ALTA o CRITICA. |
context | context | No | Dónde ocurrió: url, appVersion (app_version), browserInfo (browser_info) y customData (custom_data). |
attachmentIds | attachment_ids | No | Archivos ya subidos con el servicio de archivos. |
La categoría antigua PROBLEMA se sigue aceptando por compatibilidad y el SDK la envía como BUG; en Python avisa además con un DeprecationWarning. Usa BUG directamente.
// Primero, los adjuntos (opcional)
const screenshot = await sdk.files.upload(screenshotFile);
const ticket = await sdk.tickets.create({
user: { externalId: 'usr-4821', email: 'laura@ejemplo.com', name: 'Laura García' },
title: 'Error al generar el informe mensual',
description: 'Al pulsar «Generar informe» aparece un error y no se descarga el archivo.',
category: 'BUG',
priority: 'ALTA',
context: {
url: 'https://mi-app.com/informes',
appVersion: '2.4.1',
customData: { reportType: 'monthly' },
},
attachmentIds: [screenshot.id],
});
console.log(ticket.ticketKey); // por ejemplo, APP-0001La respuesta trae id, ticketKey (ticket_key), la clave legible que verá la persona, title, status y createdAt.
Consultar tickets
list devuelve los tickets de un usuario por páginas (20 por defecto, 100 como máximo) y admite filtrar por status. Cada elemento trae unreadByExternal (unread_by_external): los mensajes del equipo que la persona aún no ha leído. get devuelve el ticket completo con messages, attachments, context y reporter. En cada mensaje, author.type vale EXTERNAL si lo escribió la persona e INTERNAL si lo escribió el equipo de soporte.
Estados posibles: OPEN (abierto), IN_REVIEW (en revisión), CONVERTED (convertido en una tarea de Gestión de proyectos), RESOLVED (resuelto) y CLOSED (cerrado).
const result = await sdk.tickets.list('usr-4821', { status: 'OPEN', limit: 10 });
for (const item of result.data) {
const unread = item.unreadByExternal > 0 ? ' (respuestas nuevas)' : '';
console.log(`[${item.ticketKey}] ${item.title}${unread}`);
}
const detail = await sdk.tickets.get(result.data[0].id, 'usr-4821');
for (const msg of detail.messages) {
const who = msg.author.type === 'INTERNAL' ? 'Soporte' : 'Usuario';
console.log(`${who} (${msg.author.name}): ${msg.content}`);
}Responder, cerrar y reabrir
Un mensaje lleva content (de 1 a 5000 caracteres) y, si quieres, attachmentIds. close y reopen no devuelven nada; los puede hacer el usuario que abrió el ticket. getUnreadCount devuelve { count } con los mensajes sin leer de todos sus tickets.
await sdk.tickets.addMessage(ticket.id, 'usr-4821', {
content: 'He seguido los pasos y ya funciona. Gracias.',
});
await sdk.tickets.close(ticket.id, 'usr-4821');
// Si vuelve a pasar
await sdk.tickets.reopen(ticket.id, 'usr-4821');
const { count } = await sdk.tickets.getUnreadCount('usr-4821');Cambios en tiempo real
En lugar de preguntar cada poco por los mensajes sin leer, puedes abrir una conexión de eventos (Server-Sent Events) y enterarte en el momento en que el equipo responde o cambia el estado. Si indicas un usuario, solo recibes sus tickets; sin usuario, los de toda tu aplicación.
Cada evento trae ticketId, ticketKey, type y externalUserId. type solo toma dos valores, con guion medio: comment-added (hay un mensaje nuevo) y status-changed (ha cambiado el estado).
- JavaScript devuelve un objeto con
close()ykeyVersion, y avisa por las funcionesonTicketUpdatedyonError. NecesitaEventSource: en Node.js, instala un polyfill comoeventsourcey asígnalo aglobalThis.EventSource. Desde la 4.2.0 abre cada conexión con un pase de un solo uso y reconecta por sí mismo si la conexión se corta: pide un pase nuevo y espera 1, 2, 4… hasta 30 segundos entre intentos. Si el servidor rechaza el pase con un error 4xx (clave retirada o caducada, aplicación desactivada, plan sin Tickets de soporte, falta el usuario, límite de peticiones), llama aonErrory deja de intentarlo; sin respuesta o con un 5xx, vuelve a intentarlo. - Python devuelve un generador: recibes los eventos iterando con
async for(oforen la versión síncrona) y la conexión se cierra al salir del bucle. También acepta la funciónon_ticket_updated. La clave viaja en la cabeceraX-Api-Key, nunca en la dirección. No reconecta solo: si la red se corta, vuelve a llamar astream_updates.
En las aplicaciones con widget, indica siempre el usuario: sin él, la conexión se rechaza con 400 STREAM_TOKEN_USER_REQUIRED.
const stream = sdk.tickets.streamUpdates('usr-4821', {
onTicketUpdated: async (event) => {
if (event.type === 'comment-added') {
const detail = await sdk.tickets.get(event.ticketId, 'usr-4821');
// Actualiza tu interfaz con detail.messages
}
},
onError: (error) => console.error('Conexión de eventos:', error.message),
});
// Cuando ya no la necesites
stream.close();La clave de tu aplicación no viaja nunca en la dirección de la conexión, pero sigue siendo un secreto de servidor: no la pongas en el navegador de tus usuarios.
Abrir la conexión por tu cuenta
Si quieres abrir la conexión desde otro sitio, por ejemplo desde el navegador de una persona, tu servidor pide un pase de conexión con createStreamToken (create_stream_token en Python) y entrega solo el pase, nunca la clave. La respuesta trae streamToken, route, expiresAt, expiresIn (300 segundos), keyVersion, la generación de la clave con la que se emitió, y url, la dirección completa de la conexión con ?streamToken=….
- El pase vale cinco minutos y una sola conexión: para reconectar, pide otro.
- Va atado a la conexión (
route), al usuario o al trabajo de IA con el que lo pediste.ticket-eventsadmiteuserId(obligatorio en las aplicaciones con widget);ai-suggestyai-transcribeexigenjobId. - Si no se puede emitir en ese momento, la respuesta es 503
STREAM_TOKEN_UNAVAILABLEy puedes reintentar a los pocos segundos. Un pase usado o caducado se rechaza con 401STREAM_TOKEN_INVALID.
// En tu servidor
const pase = await sdk.tickets.createStreamToken({
route: 'ticket-events',
userId: 'usr-4821',
});
// Entrega pase.url al navegador; allí basta con:
// const conexion = new EventSource(pase.url);La ruta del servidor y sus errores están en Endpoints de tickets.