# Probar tu integración

En cinco minutos: diagnostica tu clave, comprueba una petición sin ejecutarla, ensaya sin escribir, llama y lee el error.

Con la clave de tu aplicación, UTILIA OS te dice qué puede llamar, qué le falta y quién lo arregla, y si una petición pasaría antes de enviarla. Nada de esto escribe datos ni emite documentos: solo la llamada real del paso 4 tiene efectos.

## Antes de empezar

- La clave de tu [aplicación externa](https://os.utilia.ai/docs/developers/api/apps-panel) (`utl_...`) en la variable de entorno `UTILIA_API_KEY`. Nunca en el código.
- Ningún permiso de la API: el diagnóstico responde también a una aplicación sin permisos.
- Para los ejemplos del SDK, la 4.6.0 o posterior. Si todavía no se puede instalar ([Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#matriz-de-versiones)), usa curl: las rutas responden igual.

## Prueba en cinco pasos

**El ciclo de cada operación**

1. **Diagnostica**: El diagnóstico dice qué puede tu clave y qué no. Si no: arregla lo tuyo y pide el resto.
2. **Comprueba**: check-request: si pasaría, sin ejecutarla. Si no: corrige el permiso o el cuerpo.
3. **Ensaya**: run o validate: prueban de verdad, sin escribir. Si no: lee los bloqueos y repite.
4. **Llama**: La ruta real. Es la única con efectos. Si no: lee errorCode, fixedBy y docsUrl.

_Las tres primeras etapas no escriben nada: repítelas tantas veces como quieras._

1. **Diagnostica tu clave**

   Una llamada te dice qué puede y qué no puede hacer tu aplicación, por qué y cuál es el siguiente paso.

   **curl**

   ```bash
   curl https://os.utilia.ai/api/external/v1/diagnostics \
     -H "X-Api-Key: $UTILIA_API_KEY"
   ```

   **SDK JavaScript**

   ```typescript
   import { UtiliaSDK } from '@utilia-os/sdk-js';

   const sdk = new UtiliaSDK({
     baseURL: 'https://os.utilia.ai/api',
     apiKey: process.env.UTILIA_API_KEY!,
   });

   const diagnostico = await sdk.diagnostics.get();
   console.log(diagnostico.verdict.title);
   for (const paso of diagnostico.nextSteps) {
     console.log(paso.fixedBy, paso.text);
   }
   ```

   **SDK Python**

   ```python
   import os

   from utilia_sdk import UtiliaSDKSync

   sdk = UtiliaSDKSync(base_url="https://os.utilia.ai/api", api_key=os.environ["UTILIA_API_KEY"])

   diagnostico = sdk.diagnostics.get()
   print(diagnostico.verdict.title)
   for paso in diagnostico.next_steps:
       print(paso.fixed_by, paso.text)
   ```

   Lee dos campos. `verdict` es el estado en una línea; `nextSteps`, los pasos en orden. Por ejemplo, para una aplicación que factura:

   ```json
   {
     "verdict": {
       "status": "attention",
       "title": "Falta un paso: la facturación de la aplicación está desactivada"
     },
     "nextSteps": [
       {
         "fixedBy": "organization",
         "text": "Pide a un administrador de tu organización que active la facturación de la aplicación. Mientras tanto, prueba con POST /external/v1/invoices/validate, que no emite nada.",
         "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/testing#billing-disabled-with-invoice-scopes"
       }
     ]
   }
   ```

   Los pasos con `fixedBy: "developer"` son tuyos. Los de `organization`, pídeselos a un administrador con el texto tal cual. Cada `docsUrl` lleva a [la explicación de su hallazgo](https://os.utilia.ai/docs/developers/getting-started/testing).
2. **Comprueba la petición**

   Antes de la primera llamada de una operación, pásala por `check-request` con el cuerpo que vas a mandar. Dice si pasaría y por qué no, con el mismo `errorCode` que daría la llamada real. No ejecuta nada ni guarda el cuerpo ([todo lo que comprueba](https://os.utilia.ai/docs/developers/getting-started/check-request)).

   **curl**

   ```bash
   curl -X POST https://os.utilia.ai/api/external/v1/diagnostics/check-request \
     -H "X-Api-Key: $UTILIA_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "method": "POST",
       "path": "/external/v1/tickets",
       "body": {
         "user": { "externalId": "user_1234" },
         "title": "No puedo entrar",
         "descripcion": "No me deja iniciar sesión desde ayer."
       }
     }'
   ```

   **SDK JavaScript**

   ```typescript
   const comprobacion = await sdk.diagnostics.checkRequest({
     method: 'POST',
     path: '/external/v1/tickets',
     body: {
       user: { externalId: 'user_1234' },
       title: 'No puedo entrar',
       descripcion: 'No me deja iniciar sesión desde ayer.',
     },
   });

   console.log(comprobacion.wouldPass, comprobacion.nextStep?.text);
   console.log(comprobacion.validation.ignoredFields); // ['descripcion']
   ```

   **SDK Python**

   ```python
   comprobacion = sdk.diagnostics.check_request(
       "POST",
       "/external/v1/tickets",
       body={
           "user": {"externalId": "user_1234"},
           "title": "No puedo entrar",
           "descripcion": "No me deja iniciar sesión desde ayer.",
       },
   )

   print(comprobacion.would_pass, comprobacion.next_step)
   print(comprobacion.validation.ignored_fields)  # ['descripcion']
   ```

   El campo se llama `description`. La respuesta, recortada, lo dice antes de enviar nada:

   ```json
   {
     "matched": true,
     "wouldPass": false,
     "validation": {
       "ignoredFields": ["descripcion"],
       "missingRequired": ["description"]
     },
     "nextStep": {
       "text": "Arregla primero esto: falta el campo obligatorio `description`.",
       "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#validation_error"
     }
   }
   ```

   `ignoredFields` es el fallo más traicionero: la llamada real respondería `201` y descartaría ese campo sin avisar.
3. **Ensaya sin escribir**

   La [prueba activa](https://os.utilia.ai/docs/developers/getting-started/check-request#prueba-activa-de-solo-lectura) hace, con cada permiso de lectura, la lectura mínima de su ruta, como tu aplicación. Devuelve `pass`, `fail` o `skip` por permiso, nunca los datos. Admite 6 al minuto.

   **curl**

   ```bash
   curl -X POST https://os.utilia.ai/api/external/v1/diagnostics/run \
     -H "X-Api-Key: $UTILIA_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{}'
   ```

   **SDK JavaScript**

   ```typescript
   const prueba = await sdk.diagnostics.run();

   console.log(prueba.summary); // { passed: 2, failed: 0, skipped: 1 }
   for (const sonda of prueba.probes) console.log(sonda.status, sonda.label);
   ```

   **SDK Python**

   ```python
   prueba = sdk.diagnostics.run()

   print(prueba.summary.passed, prueba.summary.failed, prueba.summary.skipped)
   for sonda in prueba.probes:
       print(sonda.status, sonda.label)
   ```

   Las escrituras no se prueban escribiendo: `writeScopesNotProbed` dice cómo comprobar cada una sin efectos. Para facturas, el ensayo es `POST /external/v1/invoices/validate`, con el mismo cuerpo que la emisión ([Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing)). Para los avisos a tu servidor, [Probar los avisos a tu servidor](https://os.utilia.ai/docs/developers/getting-started/webhook-testing).
4. **Llama a la ruta real**

   Con el cuerpo corregido, la llamada de verdad.

   **curl**

   ```bash
   curl -X POST https://os.utilia.ai/api/external/v1/tickets \
     -H "X-Api-Key: $UTILIA_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "user": { "externalId": "user_1234" },
       "title": "No puedo entrar",
       "description": "No me deja iniciar sesión desde ayer."
     }'
   ```

   **SDK JavaScript**

   ```typescript
   const ticket = await sdk.tickets.create({
     user: { externalId: 'user_1234' },
     title: 'No puedo entrar',
     description: 'No me deja iniciar sesión desde ayer.',
   });

   console.log(ticket.ticketKey);
   ```

   **SDK Python**

   ```python
   from utilia_sdk import CreateTicketInput, CreateTicketUser

   ticket = sdk.tickets.create(
       CreateTicketInput(
           user=CreateTicketUser(external_id="user_1234"),
           title="No puedo entrar",
           description="No me deja iniciar sesión desde ayer.",
       )
   )

   print(ticket.ticket_key)
   ```

   Si una ruta mueve dinero o emite un documento, manda una clave de idempotencia que salga de tu pedido, nunca una nueva en cada intento. Después de cada escritura, mira la cabecera `X-Utilia-Ignored-Fields`: si llega, se descartó algún campo.
5. **Lee el error**

   Si la llamada falla, el error dice qué pasa y quién lo arregla. Un ejemplo, recortado:

   ```json
   {
     "statusCode": 403,
     "errorCode": "INSUFFICIENT_SCOPE",
     "requiredScopes": ["support:tickets:write"],
     "fixedBy": "organization",
     "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#insufficient_scope",
     "retryable": false,
     "requestId": "550e8400-e29b-41d4-a716-446655440000"
   }
   ```

   **SDK JavaScript**

   ```typescript
   import { UtiliaSDKError } from '@utilia-os/sdk-js';

   try {
     await sdk.tickets.create(nuevoTicket);
   } catch (error) {
     if (!(error instanceof UtiliaSDKError)) throw error;
     console.log(error.errorCode, error.fixedBy, error.docsUrl, error.requestId);
   }
   ```

   **SDK Python**

   ```python
   from utilia_sdk import UtiliaSDKError

   try:
       sdk.tickets.create(nuevo_ticket)
   except UtiliaSDKError as error:
       print(error.error_code, error.fixed_by, error.docs_url, error.request_id)
   ```

   Decide siempre por `errorCode`. Si `fixedBy` es `organization`, no lo rodees desde el código: pídeselo a un administrador. Repite solo con `retryable: true`, y en un `429` espera lo que diga `Retry-After`. Al pedir ayuda, cita el `requestId`, nunca la clave. Cada código está explicado en [Errores y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries).

## En el panel y en la consola

La pestaña **Pruebas** de cada aplicación, en **Ajustes › Creadas por tu empresa**, enseña el mismo diagnóstico y lanza las mismas pruebas sin pedirte la clave. Cómo se usa está en [Panel de aplicaciones externas](https://os.utilia.ai/docs/developers/api/apps-panel#la-pestaña-pruebas). Además, UTILIA OS vigila la [salud de la aplicación](https://os.utilia.ai/docs/developers/getting-started/app-health) y avisa cuando deja de funcionar.

Desde la consola, `npx @utilia-os/sdk-js doctor` (o `utilia-doctor` en Python) hace el diagnóstico completo y sale con `0`, `1` o `2` para tu integración continua. Ver [La orden doctor](https://os.utilia.ai/docs/developers/getting-started/ai-assistants#la-orden-doctor).

## Si algo falla

- **`401 API_KEY_WRONG_HEADER`**: la clave va en la cabecera `X-Api-Key`, no en `Authorization`.
- **`403 APP_DISABLED` o `403 APP_RESPONSIBLE_INACTIVE`, también en el diagnóstico**: la aplicación está desactivada o no tiene persona responsable activa. Lo arregla un administrador en la ficha de la aplicación.
- **`403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY`**: la clave está publicada en una web. La prueba activa, las llamadas rechazadas y los avisos de prueba se lanzan desde la pestaña **Pruebas** ([por qué](https://os.utilia.ai/docs/developers/getting-started/diagnostics#claves-publicadas-en-una-web)).
- **`429 DIAGNOSTICS_RATE_LIMITED`**: la prueba activa admite 6 al minuto y los avisos de prueba, 5. Espera lo que diga `Retry-After`.
- **`sdk.diagnostics` no existe**: tu SDK es anterior a la 4.6.0. Llama a las rutas con curl o con tu cliente HTTP.

Cada ruta, con todos sus campos, está en [Rutas del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/diagnostics).

## Siguiente paso

- [Integrar con un asistente de IA](https://os.utilia.ai/docs/developers/getting-started/ai-assistants): La instrucción y las reglas para que tu asistente programe sin romper nada.
- [Probar la facturación](https://os.utilia.ai/docs/developers/api/invoice-testing): Validar facturas, PDF y rectificativas sin emitir nada.
