# Rutas del diagnóstico

Referencia de /me, el diagnóstico y su veredicto, las llamadas rechazadas, el contrato de tu clave y las claves publicadas, con sus cupos.

Las rutas que describen tu aplicación, con sus campos, sus cupos y lo que responden con cada clave. Para probar en cinco minutos, empieza por [Probar tu integración](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart). Lo que significa cada hallazgo está en [Hallazgos del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/testing).

## Las rutas de un vistazo

| Ruta | Qué te dice | Efecto |
| - | - | - |
| `GET /external/v1/me` | Quién es la aplicación, su clave, su cupo y sus permisos | Ninguno |
| `GET /external/v1/diagnostics` | Qué puede y qué no puede hacer, por qué y el siguiente paso | Ninguno |
| `GET /external/v1/diagnostics/calls` | Las llamadas rechazadas, agrupadas, con su arreglo | Ninguno |
| [`POST /external/v1/diagnostics/check-request`](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla) | Si una petición pasaría, sin ejecutarla | Ninguno |
| [`POST /external/v1/diagnostics/run`](https://os.utilia.ai/docs/developers/getting-started/check-request#prueba-activa-de-solo-lectura) | Si cada permiso de lectura funciona de verdad | Lecturas mínimas, sin devolver datos |
| [`POST /external/v1/diagnostics/webhook-sample`](https://os.utilia.ai/docs/developers/getting-started/webhook-testing#enviar-un-aviso-de-prueba) | Si tu servidor recibe y entiende un aviso | Un aviso de prueba a tu servidor |
| [`POST /external/v1/diagnostics/webhook-receiver-check`](https://os.utilia.ai/docs/developers/getting-started/webhook-testing#comprobar-tu-receptor-de-avisos) | Si tu servidor rechaza los avisos falsos | Tres avisos de prueba a tu servidor |
| `GET /external/v1/diagnostics/openapi.json` | El contrato con solo lo que tu clave puede llamar hoy | Ninguno |

`check-request` y la prueba activa están en [Comprobar y ensayar sin escribir](https://os.utilia.ai/docs/developers/getting-started/check-request), y los dos envíos de avisos, en [Probar los avisos a tu servidor](https://os.utilia.ai/docs/developers/getting-started/webhook-testing). Ninguna ruta escribe datos de tu organización ni emite nada: los avisos de prueba son lo único que sale hacia fuera, y llegan de verdad a tu servidor. La pestaña **Pruebas** del panel enseña el mismo diagnóstico, calculado en el mismo sitio.

### Lo que exigen

- **La clave de la aplicación en `X-Api-Key`** y ningún permiso de la API: una aplicación sin permisos puede diagnosticarse, que es justo cuando más falta le hace.
- **Ningún plan**, salvo los dos envíos de avisos, que exigen Integraciones: el trabajo del diagnóstico es explicar los bloqueos, también los de plan.
- **Con la organización suspendida o de baja** responden todas y lo dicen, menos los dos envíos de avisos, que se rechazan con `TENANT_SUSPENDED` o `TENANT_CANCELLED`.
- **Con la aplicación desactivada o sin persona responsable activa** no responde ninguna: la llamada se corta antes, como cualquier otra, con `403 APP_DISABLED` o `403 APP_RESPONSIBLE_INACTIVE`, y el `docsUrl` de ese error explica qué hacer. Por eso los hallazgos [`app-disabled`](https://os.utilia.ai/docs/developers/getting-started/testing#app-disabled), [`responsible-missing`](https://os.utilia.ai/docs/developers/getting-started/testing#responsible-missing) y [`responsible-inactive`](https://os.utilia.ai/docs/developers/getting-started/testing#responsible-inactive) solo se ven en la pestaña **Pruebas** y por MCP, con la operación `app_diagnosis`.
- **Con una clave publicada en una web**, cuatro rutas no la aceptan: ver [Claves publicadas en una web](#claves-publicadas-en-una-web).

### Cupos

**Cupos del diagnóstico**

- Rutas de diagnóstico: 30 al minuto. Por aplicación, además del cupo por hora de la clave.
- Prueba activa: 6 al minuto. Por aplicación con la clave y por persona en el panel.
- Avisos de prueba: 5 al minuto. Un solo cupo entre la clave y el panel.
- Diagnóstico guardado: 30 segundos

Todas cuentan en el cupo por hora de la clave. Al pasar el cupo de la prueba activa o el de los avisos de prueba, la respuesta es `429 DIAGNOSTICS_RATE_LIMITED`, con `Retry-After` y las cabeceras `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset` de **ese** cupo (6 o 5 al minuto, con `Remaining` a `0`), no las del freno general por minuto; el cuerpo trae `retryAfterSeconds` y `retryable: true`. Estas dos pruebas no se lanzan sin contarlas: si el contador de su cupo no responde, la respuesta es `503 DIAGNOSTICS_UNAVAILABLE`, y basta con repetir en unos minutos.

## Quién es tu aplicación

**`GET /api/external/v1/me`**

Identidad mínima y estable de la credencial: aplicación, organización (nombre y estado), clave en uso, cupo por hora y lo que queda, permisos concedidos y versión de la API. Sin permisos de la API.

```json
{
  "app": { "id": "ckapp1234567890", "name": "Tienda online", "isActive": true },
  "organization": { "name": "Ejemplo SL", "status": "active" },
  "credential": {
    "keyInUse": "current",
    "previousKeyExpiresAt": null,
    "rateLimitPerHour": 1000,
    "rateLimitRemaining": 987
  },
  "scopes": ["crm:invoices:read", "crm:invoices:write", "crm:invoices:issue"],
  "apiVersion": "v1",
  "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/testing"
}
```

Si responde `200`, la clave vale y va en la cabecera buena, y ya cuenta como [primera llamada correcta](https://os.utilia.ai/docs/developers/getting-started/testing#recorrido-first-call-ok).

- `keyInUse: "previous"`: llamas con la clave anterior durante una renovación; `previousKeyExpiresAt` dice hasta cuándo vale.
- `organization.status`: `active`, `suspended` o `cancelled`.
- Con una clave publicada en una web: `credential.publishable: true`, el estado de la organización solo dice `active` o `inactive`, y `scopes` solo lista los permisos que esa clave puede usar; los demás se cuentan en `credential.scopesOutsideCeiling`.

## El diagnóstico de tu aplicación

**`GET /api/external/v1/diagnostics`**

Qué puede y qué no puede hacer la aplicación, por qué, quién lo arregla y el siguiente paso. Con ?format=markdown devuelve las instrucciones para tu asistente de IA. Sin permisos de la API.

Se calcula en el servidor, con la configuración de la aplicación, el plan de la organización y el tráfico de las últimas 24 horas, y se guarda 30 segundos. Es la misma fuente que pinta el panel: lo que ves en la pestaña **Pruebas** y lo que lee tu programa no se contradicen.

### curl

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

# Las instrucciones para tu asistente de IA
curl "https://os.utilia.ai/api/external/v1/diagnostics?format=markdown" \
  -H "X-Api-Key: $UTILIA_API_KEY"
```

### SDK JavaScript

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

// Las instrucciones para tu asistente de IA
const instrucciones = await sdk.diagnostics.getMarkdown();
```

### SDK Python

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

# Las instrucciones para tu asistente de IA
instrucciones = sdk.diagnostics.get_markdown()
```

`sdk.diagnostics` está en los SDK a partir de la 4.6.0. Con el cliente asíncrono de Python (`async with UtiliaSDK(...) as sdk:`), cada llamada lleva `await`. Una respuesta recortada:

```json
{
  "schemaVersion": 1,
  "generatedAt": "2026-10-21T10:00:00.000Z",
  "verdict": {
    "status": "attention",
    "title": "Falta un paso: la facturación de la aplicación está desactivada",
    "detail": "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."
  },
  "app": { "id": "ckapp1234567890", "name": "Tienda online", "isActive": true, "purposes": ["billing", "webhooks"] },
  "organization": { "name": "Ejemplo SL", "status": "active" },
  "responsible": { "state": "active" },
  "findings": [
    {
      "code": "BILLING_DISABLED_WITH_INVOICE_SCOPES",
      "severity": "blocking",
      "fixedBy": "organization",
      "title": "La facturación de la aplicación está desactivada",
      "detail": "Tiene permisos de facturas, pero cada factura que intente emitir se rechaza con BILLING_NOT_ENABLED. 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",
      "action": { "kind": "enable_billing", "label": "Activar la facturación" }
    }
  ],
  "nextSteps": [
    {
      "code": "BILLING_DISABLED_WITH_INVOICE_SCOPES",
      "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"
    }
  ]
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `verdict` | `objeto` | Sí | El estado en una línea: status (ok, attention, failing o idle; paused solo en el panel y por MCP), title y detail con el único paso que hace falta. Ver Cómo se lee el veredicto. |
| `findings` | `array` | Sí | Los hallazgos, cada uno con code, severity (blocking, warning, optional o info), fixedBy, title, detail, docsUrl y, si se puede, action. Cada code tiene su apartado en Hallazgos del diagnóstico. |
| `nextSteps` | `array` | Sí | Los pasos en orden: primero lo que arreglas tú (developer), después la organización y después nosotros. |
| `access` | `array` | Sí | Por recurso, cada operación con allowed y, si no, reason (la primera causa, en el orden en que la comprueba la llamada real), missingScopes y su ensayo en seco (dryRun). Lo que el plan no incluye y la aplicación tampoco intenta usar solo se cuenta en unavailable. |
| `scopes` | `array` | Sí | Cada permiso concedido con su state: usable, not_in_plan, capability_inactive o coming_soon, y enforcementDate si está en transición. |
| `credential` | `objeto` | Sí | keyInUse, previousKeyExpiresAt, lastUsedAt, rateLimitPerHour, rateLimitRemaining y, con una clave publicada en una web, publishable: true y scopesOutsideCeiling, cuántos permisos concedidos no puede usar esa clave. |
| `organization / responsible` | `objeto` | Sí | El nombre y el estado de la organización, y el estado de la persona responsable (active, inactive o missing). Nunca su nombre ni su correo. |
| `billing` | `objeto \| null` | Sí | Si la facturación de la aplicación está activa, si el plan incluye Finanzas y si envía las facturas por correo. null si la aplicación no factura. |
| `webhooks` | `objeto` | Sí | Configuración de los avisos, eventos fuera de plan, entregas de las últimas 24 horas y el resultado de la última comprobación del receptor. |
| `traffic` | `objeto` | Sí | Última llamada, última que fue bien, recuentos de las últimas 24 horas, el pulso por horas y las llamadas rechazadas agrupadas (con resolved: true las que ya no tienen causa). No cuenta las llamadas al propio diagnóstico ni a /me. |
| `security` | `objeto` | Sí | openAdvisories: cuántas recomendaciones de seguridad hay abiertas sobre las credenciales de la aplicación. |
| `journey` | `array` | Sí | Los primeros pasos de la integración según su propósito, marcados solos con lo que observa el servidor. |

### Las instrucciones para tu asistente

Con `?format=markdown`, la misma ruta devuelve unas instrucciones sacadas del mismo diagnóstico. Nunca llevan la clave: la nombran como la variable `UTILIA_API_KEY`.

- **Conexión**: la dirección base de la API, `https://os.utilia.ai/api` (la que esperan los SDK y la variable `UTILIA_BASE_URL` de la orden `doctor`), la ruta completa de la API pública, la cabecera de la clave, el contrato `GET /external/v1/diagnostics/openapi.json` como referencia de rutas y campos, y la documentación para asistentes en `https://os.utilia.ai/llms.txt`.
- **Estado ahora**: el veredicto.
- **Qué puede llamar hoy** y **qué no y por qué**, operación por operación.
- **Lo que falta**, con quién lo arregla y su `docsUrl`.
- **Primeros pasos**: los de `journey`, hechos o pendientes, con cómo se marca cada uno.
- **Reglas** para probar sin romper nada.

### Cómo se lee el veredicto

`verdict.title` empieza siempre por una de estas frases, y la pestaña **Pruebas** enseña la misma palabra arriba del todo. Lo que sigue a los dos puntos es el hallazgo que manda.

| `title` empieza por | `status` | Cuándo |
| - | - | - |
| Funciona | `ok` | Nada que revisar. |
| Funciona en parte | `attention` | Unas rutas fallan y otras han ido bien en la última hora. No llega a **No funciona** ni avisa por correo. |
| Revisar | `attention` | Llamadas rechazadas por falta de permiso, sin otras que vayan bien. |
| Falta un paso | `attention` | Algo bloquea, pero ninguna llamada lo ha encontrado todavía: la facturación desactivada sin emisiones rechazadas, o los datos fiscales de la organización incompletos. |
| Funciona, pero hay que revisar algo | `attention` | Un aviso: funciona, con algo pendiente. |
| No funciona | `failing` | Un bloqueo que ya rechaza llamadas, por la configuración o por el tráfico. |
| Todavía no ha llamado | `idle` | La clave nunca ha recibido una respuesta correcta. Solo en el panel y por MCP: con la clave, la propia llamada al diagnóstico ya es una. |
| Todavía no usa la API | `idle` | La clave ha respondido bien (a `/me` o al diagnóstico), pero no hay llamadas a ninguna otra ruta. |
| Sin uso en los últimos 30 días | `idle` | Usó la API, pero no en los últimos 30 días. |
| Desactivada | `paused` | La aplicación está desactivada y rechaza todas sus llamadas. Solo en el panel y por MCP: con la clave, el diagnóstico responde `403 APP_DISABLED`. |

Las tres frases de `idle` siguen una sola regla. **Usar la API** es llamar a cualquier ruta salvo `GET /external/v1/me` y las del propio diagnóstico; **probar la clave** es recibir cualquier respuesta correcta con ella, también de esas dos. Es la misma regla que marca el paso [Primera llamada correcta](https://os.utilia.ai/docs/developers/getting-started/testing#recorrido-first-call-ok).

En el panel, todo `attention` que no es «Funciona en parte» se lee **Revisar**. Los mismos estados son los de [la salud de la aplicación](https://os.utilia.ai/docs/developers/getting-started/app-health).

### Por qué no se puede llamar a una operación

En `access`, cada operación que tu aplicación no puede llamar trae `reason`: la primera causa, en el orden en que la comprueba la llamada real. Cada motivo corresponde al error que daría esa llamada, así que puedes tratarlo igual que el error, sin esperar a que ocurra.

| `reason` | Qué pasa | La llamada real responde |
| - | - | - |
| `APP_DISABLED` | La aplicación está desactivada. Lo arregla la organización. Solo en el panel y por MCP: con la clave, la llamada se corta antes. | `403 APP_DISABLED` |
| `RESPONSIBLE_INACTIVE` | La aplicación no tiene persona responsable, o está dada de baja o bloqueada. Lo arregla la organización. Solo en el panel y por MCP. | `403 APP_RESPONSIBLE_INACTIVE` |
| `PUBLISHABLE_KEY_CEILING` | La clave está publicada en páginas web, la operación exige un permiso que no se usa con ella y UTILIA OS ya lo exige también desde los servidores. Lo arregla la organización, separando la integración en dos aplicaciones. | `403 SCOPE_NOT_ALLOWED_FROM_BROWSER` |
| `MODULE_NOT_IN_PLAN` | El plan no incluye el aplicativo de la operación (`moduleKey`). Lo arregla la organización. | `403 MODULE_NOT_ENABLED` |
| `COMING_SOON` | La función está en «Próximamente». No hay que hacer nada. | `403 FEATURE_COMING_SOON` |
| `CAPABILITY_NOT_ACTIVE` | El plan no tiene activa la función que necesita (`capabilityKey`). Lo arregla la organización. | `403 CAPABILITY_NOT_ENABLED` |
| `TENANT_SUSPENDED` y `TENANT_CANCELLED` | La organización está suspendida o de baja, y la operación crea o cambia datos. Las consultas siguen funcionando. | `403 TENANT_SUSPENDED` o `403 TENANT_CANCELLED` |
| `SCOPE_MISSING` | Falta un permiso de la API; `missingScopes` dice cuál. Lo concede la organización. | `403 INSUFFICIENT_SCOPE` |
| `BILLING_DISABLED` | La operación factura o cobra y la facturación de la aplicación está desactivada. Lo arregla la organización. | `403 BILLING_NOT_ENABLED` |
| `RESPONSIBLE_PERMISSION_DENIED` | La persona responsable no tiene el permiso que exige esta operación: la aplicación nunca puede más que ella. Lo arregla la organización, dándole el permiso o nombrando a otra persona. | `403 APP_RESPONSIBLE_PERMISSION_DENIED` |

Lo que el plan no incluye y la aplicación tampoco intenta usar (no tiene su permiso) no se detalla: se cuenta en `unavailable`, por motivo, con `MODULE_NOT_IN_PLAN`, `CAPABILITY_NOT_ACTIVE` o `COMING_SOON`. Con una clave publicada, las operaciones cuyo permiso no puede usar esa clave también se cuentan ahí, con `PUBLISHABLE_KEY_CEILING`, aunque hoy pasen desde un servidor.

## Llamadas rechazadas

**`GET /api/external/v1/diagnostics/calls`**

Llamadas fallidas de esta aplicación agrupadas por ruta, método, estado y errorCode, con su recuento, la primera y la última vez, el último requestId, qué significa y quién lo arregla. Sin permisos de la API.

La consulta admite `window`, con `24h` (la de por defecto) o `7d`; en los SDK, `calls({ window: '7d' })` y `calls(window="7d")`. Cada grupo usa la plantilla de la ruta, nunca la dirección con sus valores, y no incluye las llamadas al propio diagnóstico ni a `/me`. Con una clave publicada en una web, responde `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY`: esas llamadas se ven en la pestaña **Pruebas**.

```json
{
  "window": "24h",
  "rejected": [
    {
      "method": "POST",
      "route": "/external/v1/invoices",
      "statusCode": 403,
      "errorCode": "INSUFFICIENT_SCOPE",
      "count": 12,
      "firstAt": "2026-10-21T08:02:11.000Z",
      "lastAt": "2026-10-21T09:58:40.000Z",
      "lastRequestId": "550e8400-e29b-41d4-a716-446655440000",
      "fixedBy": "organization",
      "title": "A la aplicación le falta un permiso de la API para esta operación.",
      "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#insufficient_scope"
    },
    {
      "method": "GET",
      "route": "/external/v1/leads",
      "statusCode": 403,
      "errorCode": "MODULE_NOT_ENABLED",
      "count": 3,
      "firstAt": "2026-10-21T07:40:02.000Z",
      "lastAt": "2026-10-21T07:41:15.000Z",
      "lastRequestId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "fixedBy": "organization",
      "title": "El plan de la organización no incluye el aplicativo que usa esta operación.",
      "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#module_not_enabled",
      "resolved": true
    }
  ]
}
```

### Qué queda registrado

También las llamadas que se cortan antes de llegar a la operación:

- **Las que rechaza un guard**: plan, origen y freno por minuto, y también la propia clave cuando se reconoce de qué aplicación es (desactivada, sin responsable activo o llamando con la clave anterior que ya no vale).
- **Las que no llegan a ninguna ruta**, si traen una clave válida en `X-Api-Key`: una dirección que no existe (`404`, sin código), un método que la ruta no admite (`405 METHOD_NOT_ALLOWED`) y un origen que rechaza CORS (`403 CORS_ORIGIN_NOT_ALLOWED`). La comprobación previa que hace el navegador (`OPTIONS`) no lleva la clave y no deja rastro.
- **Estado `499`**: tu programa cortó la llamada antes de recibir la respuesta, casi siempre porque su tiempo de espera es más corto de lo que tarda la ruta. No sabes si se hizo: si era una escritura, compruébalo antes de repetirla.

Se guardan como mucho 10 rechazos por segundo y aplicación, para que una clave usada en bucle no llene el registro: por encima, el recuento es una muestra.

Un grupo trae `resolved: true` cuando su causa ya no existe con la configuración de hoy: la facturación ya está activada, el permiso ya está concedido, el plan ya incluye el aplicativo, o la aplicación, su responsable o la organización vuelven a estar activos. Esos grupos van al final, no generan hallazgos y no cuentan en el veredicto: son de antes del arreglo.

## El contrato de tu clave

**`GET /api/external/v1/diagnostics/openapi.json`**

El contrato OpenAPI 3 de la API pública con solo las operaciones que esta clave puede llamar hoy. Sin permisos de la API.

Es el contrato de la API pública recortado a tu clave, con la misma decisión que el diagnóstico: permisos de la API, plan de la organización, «Próximamente» y facturación. Dáselo tal cual a tu asistente de IA o a tu generador de clientes: todo lo que hay dentro se puede llamar con esa clave. En los SDK, `openApi()` y `open_api()`.

Lo que no puedes llamar no se detalla: `x-utilia-unavailable`, en la raíz, cuenta cuántas operaciones quedan fuera por cada motivo, sin decir cuáles. Con una clave publicada en una web, `PUBLISHABLE_KEY_CEILING` cuenta las operaciones cuyo permiso no puede usar esa clave y `PUBLISHABLE_KEY`, las pruebas que se lanzan desde el panel. Si responde `503`, el contrato no está disponible en ese momento; repite más tarde.

## Claves publicadas en una web

La clave de una aplicación cuenta como **publicada en páginas web** si la aplicación tiene dominios autorizados o si la clave vigente se ha usado desde un navegador desde su última renovación. Cualquiera puede copiarla de la página y usarla desde un servidor, sin cabecera `Origin`, así que para el diagnóstico esa clave es pública. Lo decide la configuración y la historia de la clave, no la petición: una cabecera `Origin` la pone quien quiere. Qué permisos admite una clave así, y cómo separar la web y el servidor en dos aplicaciones, está en [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).

**Lo que responde cada clave**

### Clave de servidor (Nunca ha estado en una página)

Todas las rutas del diagnóstico, con el diagnóstico completo.

### Clave publicada en una web (Con dominios o usada desde un navegador)

/me y el diagnóstico, recortados; check-request y el contrato, igual. Las llamadas rechazadas, la prueba activa y los avisos de prueba se lanzan desde el panel.

Con esa clave, `GET /external/v1/me` y `GET /external/v1/diagnostics` responden con `credential.publishable: true`, para que un `0` o un `null` no se lean como «no hay nada», y sin lo que solo interesa a tu organización o diría para qué más sirve la clave:

- `organization.status` solo dice `active` o `inactive`, y los hallazgos `TENANT_SUSPENDED` y `TENANT_CANCELLED` conservan su código con un texto neutro, sin hablar de la suscripción.
- Sin recomendaciones de seguridad abiertas (`security.openAdvisories` a `0`, sin `CREDENTIAL_ADVISORY_OPEN`).
- Sin el resultado de la comprobación del receptor (`webhooks.lastReceiverCheck` a `null`, sin `WEBHOOK_RECEIVER_ACCEPTS_INVALID_SIGNATURE`) ni los hallazgos `ORIGINS_UNRESTRICTED` y `PUBLISHABLE_KEY_SENSITIVE_SCOPES`.
- `scopes` y `access` solo llevan los permisos y las operaciones que esa clave puede usar. Lo demás se cuenta sin nombrarlo: en `credential.scopesOutsideCeiling` y, en cada grupo de `access`, en `unavailable` con el motivo `PUBLISHABLE_KEY_CEILING`.

`GET /diagnostics/calls`, `POST /diagnostics/run`, `webhook-sample` y `webhook-receiver-check` responden `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY` antes de gastar ningún cupo, porque enseñan el tráfico de la aplicación, leen como ella o envían avisos firmados a su servidor. La pestaña **Pruebas** del panel las lanza sin usar la clave. `check-request` y el contrato siguen disponibles: no revelan nada de la organización y dicen lo mismo que la llamada real.

Si llegan llamadas con esa clave a rutas que la aplicación no tiene concedidas, el diagnóstico no propone conceder el permiso: pueden no venir de tu web. Lo explica [`scope-missing-for-observed-calls`](https://os.utilia.ai/docs/developers/getting-started/testing#scope-missing-for-observed-calls).

## Siguiente paso

- [Errores y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries): Qué significa cada errorCode, quién lo arregla y cuándo repetir.
- [La pestaña Pruebas](https://os.utilia.ai/docs/developers/api/apps-panel#la-pestaña-pruebas): El mismo diagnóstico en el panel, con las pruebas sin la clave.
