Errores de plan
Cómo distinguir un 403 por falta de permiso de un bloqueo por plan (MODULE_NOT_ENABLED y CAPABILITY_NOT_ENABLED) y qué hacer con cada uno desde los SDK.
Actualizado el . 4 min de lectura
Incluido en todos los planes. Ver precios
Delante de cada operación de UTILIA OS hay dos puertas, y las dos responden con un HTTP 403. Esta página explica cómo distinguirlas desde el SDK, porque la reacción correcta es la contraria en cada caso.
- El plan de la organización. ¿Tiene contratado el aplicativo (Marketing, Finanzas, Chat…)? ¿Y la capacidad concreta que hace falta? Es una puerta comercial y vale para toda la organización, sin excepciones por rol.
- El permiso. ¿Esta credencial o esta persona puede hacerlo, y sobre qué datos?
En el SDK los dos llegan con code igual a FORBIDDEN. Lo que los separa es el errorCode del cuerpo de la respuesta. Si no los distingues, acabarás pidiendo permisos que ya están concedidos para desbloquear algo que en realidad no está contratado.
Un bloqueo de plan no se reintenta
Volver a llamar falla igual, consume cuota y llena tus registros de ruido. No cambia hasta que la organización amplía su plan: trátalo como un estado esperado de tu integración, no como una incidencia.
Los dos códigos
errorCode | Qué significa |
|---|---|
MODULE_NOT_ENABLED | El aplicativo entero no está en el plan. Ninguna operación de ese aplicativo va a funcionar. |
CAPABILITY_NOT_ENABLED | El aplicativo sí está contratado, pero esta capacidad concreta no. El resto del aplicativo funciona con normalidad. |
Así llega un aplicativo sin contratar:
{
"statusCode": 403,
"message": "El aplicativo Marketing no está habilitado para tu organización",
"error": "FORBIDDEN",
"timestamp": "2026-08-04T10:00:00.000Z",
"errorCode": "MODULE_NOT_ENABLED",
"moduleKey": "marketing"
}Y así, una capacidad sin contratar:
{
"statusCode": 403,
"message": "La capacidad Analítica avanzada del CRM no está incluida en el plan de tu organización",
"error": "FORBIDDEN",
"timestamp": "2026-08-04T10:00:00.000Z",
"errorCode": "CAPABILITY_NOT_ENABLED",
"capabilityKey": "crm.advanced_reports",
"capabilityLabel": "Analítica avanzada del CRM",
"requiredPlanKey": "advanced",
"requiredPlanName": "Avanzado"
}Los campos viajan en la raíz del cuerpo, no anidados. capabilityLabel es el nombre comercial de la capacidad, ya en castellano: úsalo tal cual si tienes que enseñárselo a alguien. requiredPlanKey y requiredPlanName se calculan sobre los planes publicados y pueden llegar a null cuando ningún plan visible incluye la capacidad; tu integración tiene que aguantar ese caso.
Distinguirlos en el SDK
El SDK copia esos campos en planContext (plan_context en Python) y ofrece las comprobaciones isPlanError(), isModuleNotEnabled() e isCapabilityNotEnabled() (en Python, las propiedades is_plan_error, is_module_not_enabled e is_capability_not_enabled). También exporta el ayudante suelto isPlanError(error) (is_plan_error(error)), cómodo cuando el error que capturas puede no ser del SDK. El code sigue siendo FORBIDDEN en los dos casos, para no romper a quien ya lo comprobaba.
El ejemplo usa la lista de clientes, que exige el aplicativo CRM en el plan.
import { ErrorCode, isPlanError, UtiliaSDKError } from '@utilia-os/sdk-js';
try {
await sdk.externalClients.list();
} catch (error) {
if (isPlanError(error)) {
if (error.isModuleNotEnabled()) {
console.log('Aplicativo sin contratar:', error.planContext?.moduleKey);
} else {
console.log(
'Falta la capacidad',
error.planContext?.capabilityLabel,
'- disponible en el plan',
error.planContext?.requiredPlanName ?? 'sin plan publicado',
);
}
return; // No reintentar.
}
if (error instanceof UtiliaSDKError && error.code === ErrorCode.FORBIDDEN) {
// Aquí sí falta un permiso o un scope.
}
}Qué hacer con cada uno
El servidor comprueba las puertas en este orden, y cada salida pide una reacción distinta:
Qué se comprueba antes de ejecutar una petición
Plan de la organización
¿Está el aplicativo en el plan?
Si falta, desactiva en tu integración todo lo que dependa de ese aplicativo y avisa a la organización de que hay que contratarlo.Si no: 403 MODULE_NOT_ENABLED.
Plan de la organización
¿Está la capacidad en el plan?
Si falta, sigue usando el resto del aplicativo y omite solo esa capacidad; nómbrala con capabilityLabel y el plan con requiredPlanName.Si no: 403 CAPABILITY_NOT_ENABLED.
Credencial y persona
¿Tiene el scope y el permiso?
Si falta, revisa los scopes de la credencial y los permisos de la persona.Si no: 403 sin errorCode de plan.
Se ejecuta la operación
Para no provocar el 403, consulta el plan al arrancar: la herramienta MCP org_plan, operación get, presente en todos los servidores MCP, devuelve el plan contratado, los aplicativos y capacidades con su estado y el plan que desbloquea cada uno de los que faltan. Exige el permiso «Ver organización» y el scope org:plan:view. La herramienta tiene doce operaciones y seis de ellas escriben (pedir que se amplíe el plan o reservar una llamada con UTILIA, entre otras): el detalle está en Pedir plan y reservar llamada.
Por MCP no llega el código
Por el carril MCP (POST /api/mcp/{dominio}, el que usan sdk.mcp.* y los copilotos conectados) el servidor no envía hoy ningún errorCode:
- Un aplicativo fuera de plan llega como error del protocolo con el mensaje «El aplicativo X no está habilitado para tu organización».
- Una capacidad fuera de plan llega como resultado de herramienta marcado como error, con un texto en castellano que explica qué falta y dónde se amplía. El SDK te lo entrega tal cual.
Por eso isPlanError() responde false en las llamadas MCP aunque el corte sea comercial: se apoya en un código que por esa vía no viaja, y deducirlo del texto sería adivinar. Con la API REST funciona sin problema.
La organización consulta y amplía su plan en Ajustes › Mi plan, necesitas entrar con tu cuenta, con los mismos nombres comerciales que devuelve capabilityLabel.