# Comprobar y ensayar sin escribir

Referencia de check-request, que dice si una petición pasaría sin ejecutarla, y de la prueba activa, que lee con cada permiso sin devolver datos.

Dos rutas para probar sin escribir datos: `check-request` razona sobre una petición sin llamar a la ruta, y la prueba activa (`run`) lee de verdad con cada permiso, sin devolver nada. Las dos se llaman con la clave de la aplicación en `X-Api-Key` y sin permisos de la API. El resto del diagnóstico está en [Rutas del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/diagnostics).

**Dos formas de probar sin escribir**

## check-request (Razona)

Dice si una petición concreta pasaría y por qué no, con su cuerpo. No toca la ruta. Siempre 200.

## Prueba activa (Prueba)

Hace la lectura mínima de cada permiso de lectura, como tu aplicación. Nunca devuelve los datos. Hasta 6 al minuto.

## Comprobar una petición sin ejecutarla

`check-request` responde a «¿pasaría esta petición?» sin llamar a la ruta: no lee ni escribe nada, y el cuerpo no se guarda ni se registra. Dice cada fallo con el **mismo** `errorCode` que daría la llamada real. Un ejemplo con los SDK está en [Probar tu integración](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart).

**`POST /api/external/v1/diagnostics/check-request`**

Dice si una petición a la API pública pasaría y, si no, por qué, sin llamar a la ruta. Siempre 200 con el resultado; 400 solo con una entrada mal formada. Sin permisos de la API.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `method` | `string` | Sí | GET, POST, PUT, PATCH o DELETE. |
| `path` | `string` | Sí | La ruta concreta (/external/v1/invoices/abc) o su plantilla (/external/v1/invoices/:id), con o sin /api delante y con o sin el dominio. Hasta 2.048 caracteres. Si lleva ?consulta, se comprueba junto a query. |
| `query` | `objeto` | No | Los parámetros de la consulta, como los mandaría tu programa. Hasta 100 parámetros y 16 KB. |
| `body` | `JSON` | No | El cuerpo, tal cual lo mandaría tu programa. Hasta 256 KB y 32 niveles de anidamiento. No se guarda ni se registra. |

Fuera de esos topes la respuesta es `400 VALIDATION_ERROR`, y también si la petición entera a `check-request` pasa de 300 KB: se corta antes de leerla. En los SDK, `checkRequest({ method, path, query?, body? })` y `check_request(method, path, query=..., body=...)`.

### Qué comprueba

`checks` trae una comprobación por cada cosa que mira la llamada real: `route` (la ruta existe en la API pública), `app`, `responsible`, `plan`, `capability` (también «Próximamente»), `scope`, `billing`, `query` y `body`. Cada una sale `pass`, `fail`, `warn` o `skip`, con su mensaje y, si falla, su `errorCode`. Se evalúan todas, aunque la llamada real se pararía en la primera que falla, para que veas de una vez lo que falta.

- **`scope`** sale `warn` si el permiso que falta está en transición y la llamada real todavía pasaría con un aviso. Los permisos críticos, de datos personales y financieros, no tienen transición: si falta uno, sale `fail`.
- **`app`** sale `fail` con `DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY` si compruebas, con una clave publicada en una web, una de las cuatro rutas de diagnóstico que esa clave no puede llamar.
- **`scope`** sale `fail` con `SCOPE_NOT_ALLOWED_FROM_BROWSER` si la ruta exige un permiso que no se usa desde una página y la llamada se rechazaría: porque la comprobación llega desde un navegador o porque UTILIA OS ya lo exige también desde los servidores. Con una clave publicada que hoy solo se rechazaría desde el navegador, sale `warn` con el mismo código. Ver [Claves dentro de una página web](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#claves-dentro-de-una-página-web).

### Dónde se pararía

`nextStep` no propone la primera de la lista, sino la primera en la que se **pararía** la llamada real. Ese orden no es el de `checks`: el plan va antes que el permiso, y la consulta y el cuerpo también, así que un cuerpo que no valida da `400` antes que el `403` de un permiso que falta.

**Dónde se pararía la llamada real**

1. **La clave y la aplicación**: app y responsible y, con una clave publicada en una web, el permiso que no se usa desde una página. Si no: 401 o 403 de la clave, la aplicación o su responsable.
2. **El plan**: plan y capability. Si no: 403 MODULE_NOT_ENABLED, CAPABILITY_NOT_ENABLED o FEATURE_COMING_SOON.
3. **La cuenta de la organización**: Solo en las rutas que crean o cambian datos. Si no: 403 TENANT_SUSPENDED o TENANT_CANCELLED.
4. **La consulta y el cuerpo**: query y body, contra la definición real de la ruta. Si no: 400 VALIDATION_ERROR, antes que cualquier permiso.
5. **El permiso de la API**: scope. Aquí se paran también las pruebas que una clave publicada en una web no puede lanzar. Si no: 403 INSUFFICIENT_SCOPE, con el permiso en missing.
6. **La facturación y el responsable**: billing y, en las rutas que lo miran, el permiso de la persona responsable. Si no: 403 BILLING_NOT_ENABLED o APP_RESPONSIBLE_PERMISSION_DENIED.

_nextStep propone la primera comprobación que falla en este orden. Una ruta que no existe se para antes de todo, con matched: false._

El ejemplo, recortado, es una factura con un permiso que falta, una línea sin `taxType` y un campo mal escrito. Fallan el permiso y el cuerpo, y `nextStep` propone el cuerpo, porque la llamada real respondería `400` antes de mirar el permiso:

```json
{
  "matched": true,
  "route": {
    "id": "POST /external/v1/invoices",
    "method": "POST",
    "path": "/external/v1/invoices",
    "resource": "invoices",
    "summary": "Crear y emitir una factura",
    "sideEffect": "irreversible"
  },
  "wouldPass": false,
  "checks": [
    { "key": "route", "status": "pass", "message": "Ruta pública: Crear y emitir una factura" },
    { "key": "scope", "status": "fail", "errorCode": "INSUFFICIENT_SCOPE", "message": "A la aplicación le falta el permiso de la API «Emitir facturas» (crm:invoices:issue). Lo concede un administrador de la organización en UTILIA OS, en la ficha de la aplicación, Configuración › Ajustes › Permisos.", "missing": ["crm:invoices:issue"] },
    { "key": "body", "status": "fail", "errorCode": "VALIDATION_ERROR", "message": "Falta el campo obligatorio `lines[0].taxType`.", "missing": ["lines[0].taxType"] }
  ],
  "validation": {
    "errors": [{ "path": "lines[0].taxType", "message": "Falta este campo obligatorio.", "constraint": "isDefined" }],
    "ignoredFields": ["lines[0].precio"],
    "missingRequired": ["lines[0].taxType"]
  },
  "nextStep": {
    "text": "Arregla primero esto: falta el campo obligatorio `lines[0].taxType`.",
    "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#validation_error"
  }
}
```

### Campos ignorados y rutas que no existen

`ignoredFields` son los campos que la llamada real descartaría sin error: casi siempre un nombre mal escrito, como `precio` en vez de `unitPrice`. Es lo mismo que te diría después la cabecera `X-Utilia-Ignored-Fields`, pero antes de enviar nada. Si el cuerpo no tiene otros errores, `body` sale `warn` y los nombra en su mensaje. Cuando todo pasa, `nextStep` propone el ensayo en seco de la ruta si lo tiene (en una factura, `POST /external/v1/invoices/validate`) o avisa de que la llamada tiene efectos.

Una ruta que no es de la API pública, también si existe en UTILIA OS, responde `matched: false` igual que una que no existe, con hasta tres rutas públicas parecidas en `suggestions`. Para saber todo lo que puedes llamar, pide [el contrato de tu clave](https://os.utilia.ai/docs/developers/getting-started/diagnostics#el-contrato-de-tu-clave).

## Prueba activa de solo lectura

**`POST /api/external/v1/diagnostics/run`**

Por cada permiso de lectura concedido, hace la lectura mínima de su ruta y dice si funciona, sin devolver datos. Si la aplicación factura, valida en seco una factura de ejemplo. Hasta 6 por minuto. Sin permisos de la API.

Donde `check-request` razona, `run` prueba: llama de verdad a cada ruta de lectura, como tu aplicación y dentro de tu organización, y devuelve solo `pass`, `fail` (con el `errorCode` que daría la ruta) o `skip` (con el motivo: fuera de plan, sin ruta de lectura sin identificador...). Nunca devuelve el contenido. Cada lectura espera 10 segundos como mucho.

```json
{
  "runAt": "2026-10-21T10:00:00.000Z",
  "durationMs": 840,
  "summary": { "passed": 2, "failed": 0, "skipped": 1 },
  "probes": [
    { "id": "invoices.list", "resource": "invoices", "label": "Leer facturas", "scope": "crm:invoices:read", "route": "GET /external/v1/invoices", "status": "pass", "message": "La lectura funciona con este permiso.", "durationMs": 210 }
  ],
  "invoiceDryRun": {
    "status": "fail",
    "message": "La factura de ejemplo no se podría emitir ahora mismo: revisa los bloqueos.",
    "blockers": [{ "code": "BILLING_NOT_ENABLED", "message": "La facturación de esta aplicación está desactivada, así que todavía no puede emitir facturas. La activa un Super Administrador de la organización en UTILIA OS, en la ficha de la aplicación, Dinero › Facturación («Activar la facturación»)." }],
    "warnings": []
  },
  "writeScopesNotProbed": [
    "Emitir facturas (crm:invoices:issue): no se prueba escribiendo. Compruébalo sin efectos con POST /external/v1/invoices/validate."
  ]
}
```

- **Las escrituras no se prueban escribiendo**: `writeScopesNotProbed` dice cómo comprobar cada una sin efectos.
- **Una factura**: `run` ya valida una de ejemplo, pero con tu cuerpo real es como sabrás si la tuya pasa. Las tres rutas que calculan sin emitir (`POST /external/v1/invoices/validate`, `POST /external/v1/invoices/validate/pdf` y `POST /external/v1/invoices/:id/rectifications/validate`) están en [Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing).
- **El resultado se reutiliza 60 segundos**, salvo que cambie la configuración de la aplicación: un permiso concedido o la facturación activada dan un resultado nuevo en el acto.
- **No cuenta como llamadas de tu aplicación**: las lecturas quedan en el registro de acceso a datos de la organización como hechas por tu aplicación, pero no aparecen en las llamadas rechazadas, no cambian su salud ni marcan pasos del recorrido.
- **Con la organización suspendida o de baja** sigue respondiendo, porque no escribe nada. Si el contador de su cupo no responde, `503 DIAGNOSTICS_UNAVAILABLE`. Con una clave publicada en una web, `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY` antes de gastar ningún cupo: lánzala desde la pestaña **Pruebas**.

## Si algo falla

- **`400 VALIDATION_ERROR` en `check-request`**: la entrada está mal formada, pasa de los topes de la tabla de campos o la petición entera pasa de 300 KB. Si lo que no valida es la petición que compruebas, la respuesta es `200`, con el fallo dentro.
- **`matched: false`**: la ruta no es de la API pública. Mira `suggestions` o [el contrato de tu clave](https://os.utilia.ai/docs/developers/getting-started/diagnostics#el-contrato-de-tu-clave).
- **`429 DIAGNOSTICS_RATE_LIMITED` en la prueba activa**: pasaste de 6 al minuto. Espera lo que diga `Retry-After`.
- **`503 DIAGNOSTICS_UNAVAILABLE` en la prueba activa**: el contador de su cupo no responde y no se lanza sin contarla. Repite en unos minutos.
- **`403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY` en la prueba activa**: la clave está [publicada en una web](https://os.utilia.ai/docs/developers/getting-started/diagnostics#claves-publicadas-en-una-web). Lánzala desde la pestaña **Pruebas** del panel; `check-request` sí funciona con esa clave.

## Siguiente paso

- [Probar la facturación](https://os.utilia.ai/docs/developers/api/invoice-testing): Validar facturas, PDF y rectificativas sin emitir nada, con tu cuerpo real.
- [Errores y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries): Qué significa cada errorCode que devuelven las comprobaciones.
