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 . 4 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 |
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()y avisa por las funcionesonTicketUpdatedyonError. NecesitaEventSource: en Node.js, instala un polyfill comoeventsourcey asígnalo aglobalThis.EventSource. - 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.
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 conexión de eventos lleva la credencial en la dirección, así que úsala desde tu servidor, nunca desde el navegador de tus usuarios. En procesos de larga duración, vuelve a conectar si la red se corta.