Gestionar errores
Qué trae un UtiliaSDKError, cómo se traduce cada respuesta HTTP, qué se reintenta y cómo reaccionar a cada caso en JavaScript y Python.
Actualizado el . 9 min de lectura
Incluido en todos los planes. Ver precios
Todo lo que falla en el SDK llega como un UtiliaSDKError: una respuesta de error de la API, un fallo de red o un dato que el propio SDK rechaza antes de enviarlo. Así puedes tratar todos los casos con un único bloque de captura y decidir qué hacer mirando un código, no el texto del mensaje.
Qué trae el error
| JavaScript | Python | Qué contiene |
|---|---|---|
code | code | Tipo de error, del enumerado ErrorCode (tabla siguiente). |
message | message | Texto legible. Siempre es una cadena. |
statusCode | status_code | Código HTTP de la respuesta, o vacío si la petición no llegó a enviarse. |
errorCode | error_code | Código de negocio del servidor, por ejemplo TICKET_NOT_FOUND. Es lo que debes comparar para casos concretos. |
requestId | request_id | Identificador de la petición (x-request-id), para buscarla en los registros del servidor. |
timestamp | timestamp | Momento del error (Date en JavaScript; datetime en UTC en Python). |
details | details | Detalle estructurado que mande el servidor. No lo enseñes tal cual a tus usuarios. |
validationErrors | No existe | Solo JavaScript: lista normalizada de errores de validación por campo, para resaltar campos o registrar. |
planContext | plan_context | Solo en los bloqueos por plan: qué aplicativo o capacidad falta. Ver Errores de plan. |
fixedBy | fixed_by | Quién lo arregla, según el servidor: developer (quien programa la integración), organization (un administrador de la organización) o utilia. Desde la 4.6.0. |
docsUrl | docs_url | El apartado de la documentación que explica ese código. Desde la 4.6.0. |
requiredScopes | required_scopes | En el 403 INSUFFICIENT_SCOPE, los permisos de la API que faltan: se los pides a la organización. Desde la 4.6.0. |
retryAfterMs | retry_after | Cuánto pide esperar el servidor antes de repetir (milisegundos en JavaScript, segundos en Python). Llega con el 429 y con el 503 que la traen. Desde la 4.6.0. |
readErrorGuidance (read_error_guidance) saca esos tres primeros datos de cualquier cuerpo de error, también si llamas a la API por tu cuenta. Qué significa cada errorCode y qué hacer con él está en Errores, identificador de petición y reintentos.
Códigos y respuestas HTTP
ErrorCode | Cuándo aparece |
|---|---|
VALIDATION_ERROR | HTTP 400 o 422, o un dato que el SDK rechaza antes de enviar. |
UNAUTHORIZED | HTTP 401: clave no válida o ausente, o sesión OAuth que ya no se puede renovar. |
FORBIDDEN | HTTP 403: aplicación desactivada, origen no permitido, scope o permiso insuficiente, o bloqueo por plan. |
NOT_FOUND | HTTP 404. |
CONFLICT | HTTP 409: la operación choca con el estado actual del recurso. El motivo va en errorCode. Desde la 4.6.0; antes llegaba como UNKNOWN, así que si tu código distinguía el conflicto con UNKNOWN, compáralo con CONFLICT o, mejor, con statusCode 409 y su errorCode. |
RATE_LIMITED | HTTP 429, o un 403 cuyo mensaje habla de «rate limit». |
NETWORK_ERROR | Sin respuesta del servidor: tiempo agotado, conexión rechazada o cortada. |
UNKNOWN | HTTP 500, 502, 503 o 504, y cualquier otro código no previsto. |
AUTH_EXPIRED | Declarado, pero el SDK no lo emite hoy: una sesión caducada llega como UNAUTHORIZED. |
Comprobaciones rápidas
El error trae comprobaciones ya hechas. En JavaScript son métodos (con paréntesis); en Python, propiedades (sin paréntesis).
| JavaScript | Python | Verdadero cuando |
|---|---|---|
isRetryable() | is_retryable | Tiene sentido volver a intentarlo: RATE_LIMITED o NETWORK_ERROR, salvo que el servidor diga expresamente lo contrario. |
isRateLimited() | is_rate_limited | Es un límite de peticiones. |
isUnauthorized() | is_unauthorized | La credencial no vale. |
isPlanError() | is_plan_error | Lo bloquea el plan de la organización, no un permiso. |
isModuleNotEnabled() | is_module_not_enabled | Falta el aplicativo entero. |
isCapabilityNotEnabled() | is_capability_not_enabled | El aplicativo está, pero falta una capacidad concreta. |
Desde la 4.6.0, la función isInsufficientScopeError(error) (is_insufficient_scope_error(error)) dice si falta un permiso de la API, y error.requiredScopes (error.required_scopes), cuál.
Para registrar el error en tus trazas, toJSON() (to_dict() en Python) lo convierte en un objeto plano con el código, el mensaje, la fecha, el estado HTTP, el identificador de petición y el contexto de plan.
Capturar y reaccionar
Mira primero si es un bloqueo de plan y después el code, nunca el texto del mensaje. Cuando el error te llega, el SDK ya ha reintentado lo que tenía sentido reintentar.
¿Qué hago con este error?
¿Lo bloquea el plan de la organización (isPlanError)?
Sí:
MODULE_NOT_ENABLED o CAPABILITY_NOT_ENABLED
Ni reintentar ni dar permisos lo arregla: planContext dice qué aplicativo o capacidad falta.No:
¿Qué code trae el error?
VALIDATION_ERROR:
Sin statusCode, lo rechazó el SDK antes de enviar; con 400 o 422, el servidor. Repetirlo igual da el mismo error.UNAUTHORIZED:
401
La clave no vale o falta, o la sesión OAuth ya no se puede renovar: pide a la persona que vuelva a autorizar.FORBIDDEN:
403
Con errorCode INSUFFICIENT_SCOPE, requiredScopes dice qué permiso de la API falta; si no, la aplicación está desactivada, sin responsable activo o el origen no está permitido.Otro código:
¿Cuál de los demás?
NOT_FOUND o CONFLICT:
Revisa el recurso
404 o 409
El errorCode dice qué no existe o con qué estado choca, por ejemplo TICKET_NOT_FOUND. Repetirlo igual no lo arregla, salvo con retryable: true.RATE_LIMITED:
429
El SDK ya esperó lo que pedía el servidor. Si la espera pasaba de maxRetryAfterMs, no reintentó: retryAfterMs dice cuánto esperar.NETWORK_ERROR:
Mira si se hizo antes de repetir
No hubo respuesta. Las altas sin clave de idempotencia y las escrituras que mueven dinero no se reintentan y pudieron hacerse: consulta su estado primero.UNKNOWN:
Fallo del servidor
5xx
El SDK ya lo reintentó. Guarda el requestId para soporte y, si era una escritura que mueve dinero, consulta su estado antes de repetirla.
El mismo criterio, en código:
import { ErrorCode, UtiliaSDKError } from '@utilia-os/sdk-js';
try {
await sdk.tickets.create({
user: { externalId: 'usr_123' },
title: 'Error en el formulario de pago',
description: 'Al enviar el formulario aparece un error 500 en la consola.',
});
} catch (error) {
if (!(error instanceof UtiliaSDKError)) throw error;
if (error.isPlanError()) {
// No se arregla reintentando ni dando permisos: hay que contratar.
return;
}
switch (error.code) {
case ErrorCode.VALIDATION_ERROR:
console.error('Datos no válidos:', error.message);
break;
case ErrorCode.UNAUTHORIZED:
console.error('Revisa la clave o la sesión OAuth.');
break;
case ErrorCode.FORBIDDEN:
// Desde la 4.6.0, requiredScopes dice qué permiso de la API falta.
console.error('Falta un permiso:', error.requiredScopes ?? error.errorCode, error.docsUrl);
break;
case ErrorCode.NOT_FOUND:
console.error('No existe:', error.errorCode);
break;
case ErrorCode.CONFLICT:
console.error('Choca con el estado del recurso:', error.errorCode);
break;
default:
console.error(`[${error.code}] ${error.message} (petición ${error.requestId})`);
}
}Errores de las credenciales
Desde la 4.2.0, los dos SDK conocen y tipan los códigos de negocio de las credenciales. En JavaScript, CREDENTIAL_ERROR_CODES los reúne, isCredentialErrorCode(error.errorCode) dice si un código es de esa familia y el tipo KnownErrorCode tipa error.errorCode. En Python, CredentialErrorCode (comparable con error.error_code) e is_credential_error_code. Los que más vas a ver:
| errorCode | HTTP | Qué hacer |
|---|---|---|
APP_RESPONSIBLE_INACTIVE | 403 | La persona responsable de la aplicación ya no está activa. Pide en la organización que nombren a otra en la ficha de la aplicación; reintentar no lo arregla. |
API_KEY_WRONG_KIND | 401 | La credencial es de otro tipo: una clave de la organización (utk_) contra la API externa, o la clave de la aplicación (utl_) en Authorization: Bearer contra una ruta del panel. Usa la credencial de esa API. Tipado desde la 4.6.0. |
API_KEY_WRONG_HEADER | 401 | La clave de la aplicación llegó en Authorization: Bearer y no en X-Api-Key. El SDK la pone siempre en su sitio: solo aparece si llamas por tu cuenta. Tipado desde la 4.6.0. |
AUTH_TOKEN_INVALID | 401 | Una ruta del panel no acepta el token de sesión: falta, está mal formado o ha caducado. No llega desde la API externa. Tipado desde la 4.6.0. |
API_KEY_IN_URL_NOT_ALLOWED | 401 | La clave llegó en la dirección. Envíala en la cabecera X-Api-Key y, para una conexión en tiempo real, pide antes un pase de conexión (el SDK lo hace solo desde la 4.2.0). |
STREAM_TOKEN_INVALID | 401 | El pase de conexión ya se usó o ha caducado. Pide otro. |
STREAM_TOKEN_USER_REQUIRED | 400 | Una aplicación con widget abrió la conexión de eventos sin el usuario. |
STREAM_TOKEN_UNAVAILABLE | 503 | No se pudo emitir el pase. Se puede reintentar a los pocos segundos. |
OAUTH_BEARER_NOT_ALLOWED | 401 | La ruta exige la sesión de una persona y no admite el token OAuth. El SDK no renueva el token ni repite la petición. |
METHOD_NOT_AVAILABLE_WITH_API_KEY | Sin respuesta | Lo lanza el propio SDK, con code FORBIDDEN y sin hacer la petición, cuando el método va a una ruta que la clave de la aplicación no alcanza. El mensaje dice qué usar en su lugar. |
Cambio en la 4.2.0 si decidías por el código
Una aplicación cuya persona responsable no está activa recibía antes 401 API_KEY_INVALID o 422 EXTERNAL_APP_CREATOR_MISSING; ahora recibe 403 APP_RESPONSIBLE_INACTIVE. EXTERNAL_APP_CREATOR_MISSING queda solo para una aplicación sin organización vinculada. Si tu código decidía por los códigos antiguos, contempla el nuevo.
Confirmar una operación en el móvil
Algunas operaciones MCP, como renovar la clave de una aplicación externa o su clave de firma, piden a la persona que las confirme en el móvil si tiene la verificación en dos pasos. La primera llamada falla con APPROVAL_REQUIRED, y getApprovalChallenge(error) (get_approval_challenge en Python) devuelve approvalId, el número (number) que la persona tiene que elegir y expiresAt. Cuando lo aprueba, repite exactamente la misma llamada con twoFactorApprovalId (two_factor_approval_id). isApprovalRequired(error) (is_approval_required) responde si el error es de este tipo. El SDK nunca acepta códigos de verificación por esta vía.
import { getApprovalChallenge } from '@utilia-os/sdk-js';
try {
await sdk.mcp.integrations.appsRegenerateApiKey(appId, { graceDays: 7 });
} catch (error) {
const reto = getApprovalChallenge(error);
if (!reto) throw error;
console.log(`Elige el número ${reto.number} en el móvil.`);
// Cuando la persona lo apruebe:
await sdk.mcp.integrations.appsRegenerateApiKey(appId, {
graceDays: 7,
twoFactorApprovalId: reto.approvalId,
});
}Si la persona no puede aprobarla en su móvil, renueva la clave desde la ficha de la aplicación, con su código de verificación.
Reintentos
El SDK ya reintenta por su cuenta los errores de red, los 429 y los 5xx, hasta el número total de intentos de retryAttempts (retry_attempts), que por defecto es 3. Desde la 4.6.0:
- Espera lo que pide el servidor. Con
Retry-Afterespera ese tiempo.X-Utilia-Quota-Resetsolo cuenta en el 429 del cupo por hora agotado que no traeRetry-After; un 5xx u otro 429 sinRetry-Afterse repite con la espera creciente de siempre: 0,5 s, 1 s, 2 s… Si el servidor pide más demaxRetryAfterMs(max_retry_after, un minuto por defecto), como cuando se agota el cupo por hora, no reintenta: el error llega enseguida conretryAfterMs(retry_after). - No repite lo que podría duplicar. Un
POSTsin clave de idempotencia (una alta de ticket, mensaje, cliente, lead, archivo o IA) se intenta una sola vez, y las escrituras que mueven dinero, comoinvoices.createeinvoices.cancel, también. Si repites una a mano, conserva la clave de idempotencia y los datos originales.
Los detalles están en Configurar el cliente.
El envío de errores va aparte: errors.reportBatch y ai.reportError (report_batch, report_error) hacen un solo intento, y la cola de errors.install y capture tiene sus propios reintentos, que nunca repiten antes de lo que pide Retry-After (Capturar los errores de tu aplicación).
Si prefieres controlar tú los reintentos, pon retryAttempts: 1 (o retry_attempts=1), usa la comprobación isRetryable y, cuando el servidor diga cuánto esperar, respeta retryAfterMs (retry_after, en segundos):
import { UtiliaSDKError } from '@utilia-os/sdk-js';
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (error) {
const retryable = error instanceof UtiliaSDKError && error.isRetryable();
if (!retryable || attempt === maxAttempts) throw error;
const waitMs = error.retryAfterMs ?? 1000 * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
}
const ticket = await withRetry(() => sdk.tickets.get('ticket-uuid', 'usr_456'));Errores antes de enviar la petición
El SDK comprueba algunos datos antes de llamar a la API. Cuando falla una de esas comprobaciones, el error no tiene código HTTP ni identificador de petición, porque la petición no llegó a salir.
Dónde salta un dato no válido
JavaScript
En el método del servicio
Python
Al construir el modelo
La lista completa de comprobaciones y sus límites está en Límites y validaciones.