# Consultar y gestionar los errores

Lista, cuenta y abre los errores que ha enviado tu aplicación, y resuélvelos, reábrelos o siléncialos desde tu código, como tu equipo en el panel.

Tu aplicación puede leer los errores que ella misma ha enviado y gestionarlos igual que tu equipo en la [pestaña Errores](https://os.utilia.ai/docs/developers/api/errors-tab): resolverlos cuando sale la corrección, reabrirlos o silenciarlos durante un incidente que ya conoces. Solo ve los suyos, nunca los de otra aplicación ni los internos de UTILIA OS, y cada cambio avisa a tu servidor. Cómo se envían está en [Enviar los errores de tu aplicación](https://os.utilia.ai/docs/developers/api/errors).

## Autenticación y permisos

Todas las peticiones llevan la cabecera `X-Api-Key`, y el plan de tu organización tiene que incluir el aplicativo **Integraciones**.

| Endpoint | Permiso de la API (scope) |
| - | - |
| `GET /external/v1/errors` | `analytics:errors:read` |
| `GET /external/v1/errors/stats` | `analytics:errors:read` |
| `GET /external/v1/errors/:hash` | `analytics:errors:read` |
| `POST /external/v1/errors/:hash/resolve` | `analytics:errors:manage` |
| `POST /external/v1/errors/:hash/reopen` | `analytics:errors:manage` |
| `POST /external/v1/errors/:hash/silence` | `analytics:errors:manage` |
| `DELETE /external/v1/errors/:hash/silence` | `analytics:errors:manage` |

Las cuatro rutas de gestión actúan en nombre de la persona responsable de la aplicación. Esa persona necesita además el permiso **Gestionar los errores de las aplicaciones externas**, el mismo que pide la pestaña Errores: por la API no se puede hacer más que en el panel. Cada acción queda en el registro de auditoría a su nombre.

Como el envío, estas rutas admiten 120 peticiones por minuto cada una, por aplicación. A diferencia del envío, sí cuentan en el cupo por hora de tu clave, como cualquier otra petición de la API.

## Listar los errores

**`GET /api/external/v1/errors`**

Grupos de errores que ha enviado esta aplicación, con filtros opcionales. Permiso: analytics:errors:read.

### Parámetros de consulta

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `severity` | `string \| string[]` | No | critical, high, medium o low. Para varias, repite el parámetro (severity=critical\&severity=high) o sepáralas con comas. |
| `status` | `string` | No | active (sin resolver y sin silencio vigente), resolved, silenced o all. Sin status ni resolved, se devuelven todos. |
| `resolved` | `boolean` | No | Filtro anterior a status, que se mantiene: true, resueltos; false, sin resolver, también los silenciados. Se ignora si llega status. |
| `module` | `string` | No | Módulo exacto, hasta 100 caracteres. |
| `firstRelease` | `string` | No | Versión exacta en la que el error apareció por primera vez: los nuevos de la 2.4.1. |
| `from` | `string` | No | Última ocurrencia desde esta fecha, en ISO 8601 (2026-10-01T00:00:00Z). |
| `to` | `string` | No | Última ocurrencia hasta esta fecha, en ISO 8601. |
| `limit` | `number` | No | Tamaño de la página, de 1 a 200. Por defecto, 50. |
| `offset` | `number` | No | Cuántos grupos saltar, para paginar. Por defecto, 0. |

Los grupos salen de más a menos graves y, dentro de cada gravedad, del más reciente al más antiguo.

### Respuesta

```json
{
  "data": [
    {
      "id": "cm9x2k1ab0001",
      "hash": "5f2b7c1e9a0d4e3b8c6a1f7d2e9b0c4a",
      "message": "Tiempo de espera agotado con la pasarela",
      "module": "pagos",
      "severity": "critical",
      "file": "src/pagos.ts",
      "line": 45,
      "endpoint": "/api/pagos/cobrar",
      "count": 37,
      "firstOccurrence": "2026-10-20T09:12:00.000Z",
      "lastOccurrence": "2026-10-22T08:41:00.000Z",
      "resolved": false,
      "resolvedAt": null,
      "status": "active",
      "silencedUntil": null,
      "environment": "production",
      "firstRelease": "2.4.0",
      "lastRelease": "2.4.2",
      "regressionCount": 1,
      "lastRegressedAt": "2026-10-22T08:41:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "totalPages": 1
  }
}
```

| Nombre | Tipo | Descripción |
| - | - | - |
| `status` | `string` | active (sin resolver y sin silencio vigente), resolved o silenced. |
| `silencedUntil` | `string \| null` | Hasta cuándo está silenciado, si lo está. |
| `environment` | `string \| null` | Entorno declarado al enviarlo. El entorno no separa grupos: si el mismo fallo llega de varios, queda el primero. |
| `firstRelease, lastRelease` | `string \| null` | Primera y última versión en que se vio. |
| `regressionCount` | `number` | Veces que ha vuelto a ocurrir después de darse por resuelto. |
| `lastRegressedAt` | `string \| null` | Última vez que volvió tras darse por resuelto. |
| `pagination` | `object` | page (offset / limit + 1), limit, total y totalPages. |

### Ejemplos

#### curl

```bash
curl 'https://os.utilia.ai/api/external/v1/errors?status=active&severity=critical&severity=high&limit=20' \
  -H 'X-Api-Key: utl_xxx'
```

#### JavaScript

```typescript
const { data, pagination } = await sdk.errors.list({
  status: 'active',
  severity: ['critical', 'high'],
  limit: 20,
});
console.log(`${pagination.total} pendientes`, data.map((e) => e.message));
```

#### Python

```python
from utilia_sdk import ErrorFilters

pagina = await sdk.errors.list(
    ErrorFilters(status="active", severity=["critical", "high"], limit=20)
)
print(pagina.pagination.total, [e.message for e in pagina.data])
```

En Python, `from` y `to` se escriben `from_date` y `to_date`, porque `from` es palabra reservada, y `firstRelease`, `first_release`.

## Estadísticas

**`GET /api/external/v1/errors/stats`**

Cifras de los errores de esta aplicación. Permiso: analytics:errors:read.

```json
{
  "total": 156,
  "unresolved": 42,
  "silenced": 3,
  "bySeverity": { "critical": 5, "high": 18, "medium": 12, "low": 7 },
  "byModule": { "pagos": 15, "auth": 8, "importador": 19 },
  "topErrors": [
    {
      "hash": "5f2b7c1e9a0d4e3b8c6a1f7d2e9b0c4a",
      "message": "Tiempo de espera agotado con la pasarela",
      "count": 37,
      "severity": "critical"
    }
  ]
}
```

- `total`: grupos de la aplicación, en cualquier estado.
- `unresolved`: los pendientes, sin resolver y sin silencio vigente. Es la misma cifra que la pestaña Errores.
- `silenced`: los que están sin resolver con un silencio vigente.
- `bySeverity` y `byModule`: los pendientes, por gravedad y por módulo.
- `topErrors`: los diez pendientes con más ocurrencias, con el mensaje recortado a 100 caracteres.

Con los SDK, `sdk.errors.stats()`.

## Detalle de un error

**`GET /api/external/v1/errors/:hash`**

Un grupo con su pila, su contexto y lo que tiene vinculado. Permiso: analytics:errors:read.

Parámetro de ruta: `hash`.

Devuelve los campos del listado y, además:

```json
{
  "hash": "5f2b7c1e9a0d4e3b8c6a1f7d2e9b0c4a",
  "status": "active",
  "stack": "Error: Tiempo de espera agotado\n    at cobrar (src/pagos.ts:45:12)",
  "function": "cobrar",
  "method": "POST",
  "context": {
    "pedidoId": "ped_456",
    "tags": { "pasarela": "redsys" },
    "externalUserRef": "usr-4821"
  },
  "resolutionNote": null,
  "lastRequestId": "req-pagos-0042",
  "linkedTicket": { "id": "tkt_123", "key": "SUP-482" },
  "linkedIssue": null
}
```

- `stack` y `context` llegan saneados, sin credenciales ni correos. El contexto es el de la última ocurrencia; las etiquetas están en `context.tags` y el `userId` que enviaste, en `context.externalUserRef`.
- `lastRequestId`: el identificador de petición de la última ocurrencia que lo traía.
- `linkedTicket` y `linkedIssue`: el ticket de soporte o la tarea de Gestión de proyectos vinculados, con su clave visible, o `null`.

Un hash que no existe, que es de otra aplicación o que no es tuyo responde el mismo `404`. Con los SDK, `sdk.errors.get(hash)`.

## Resolver, reabrir y silenciar

Las cuatro acciones cambian el estado del grupo igual que los botones de la pestaña Errores, y cada una avisa a tu servidor. Los estados y qué los cambia están en [Estados de un grupo de errores](https://os.utilia.ai/docs/developers/api/errors-tab#estados-de-un-grupo-de-errores). Responden `200`.

### Resolver

**`POST /api/external/v1/errors/:hash/resolve`**

Marca el grupo como resuelto, con una nota opcional. Permiso: analytics:errors:manage.

Parámetro de ruta: `hash`.

`note` es opcional, de hasta 1.000 caracteres. La respuesta es `{ "hash": "…", "resolved": true, "alreadyResolved": false }` y tu servidor recibe `ERROR_RESOLVED`. Si ya estaba resuelto, `alreadyResolved: true`: no cambia nada ni sale ningún aviso.

Si el error vuelve a ocurrir, **se reabre solo**: esa ocurrencia responde `status: regressed`, el grupo suma una en `regressionCount` y tu servidor recibe `ERROR_REOPENED` con `reason: regression` y la versión en `release`.

### Reabrir

**`POST /api/external/v1/errors/:hash/reopen`**

Devuelve a pendiente un grupo resuelto. Permiso: analytics:errors:manage.

Parámetro de ruta: `hash`.

Sin cuerpo. La respuesta es `{ "hash": "…", "alreadyOpen": false }` y tu servidor recibe `ERROR_REOPENED` con `reason: manual`. Si ya estaba abierto, `alreadyOpen: true` y no sale ningún aviso.

### Silenciar y quitar el silencio

**`POST /api/external/v1/errors/:hash/silence`**

Silencia el grupo de 1 a 672 horas. Permiso: analytics:errors:manage.

Parámetro de ruta: `hash`.

`hours` es obligatorio, un entero de 1 a 672 (cuatro semanas). Mientras dure, el grupo sigue sumando ocurrencias, pero no cuenta como pendiente ni avisa. Volver a silenciarlo sustituye el plazo. La respuesta trae el fin del silencio en `silencedUntil` y tu servidor recibe `ERROR_SILENCED` con esa fecha en `until`.

**`DELETE /api/external/v1/errors/:hash/silence`**

Quita el silencio antes de tiempo. Permiso: analytics:errors:manage.

Parámetro de ruta: `hash`.

La respuesta es `{ "hash": "…", "silencedUntil": null }`; si el grupo no está resuelto, vuelve a contar como pendiente. Tu servidor recibe `ERROR_SILENCED` con `until: null`.

### Ejemplos

#### curl

```bash
HASH=5f2b7c1e9a0d4e3b8c6a1f7d2e9b0c4a

curl -X POST "https://os.utilia.ai/api/external/v1/errors/$HASH/resolve" \
  -H 'Content-Type: application/json' -H 'X-Api-Key: utl_xxx' \
  -d '{ "note": "Corregido en la 2.4.2" }'

curl -X POST "https://os.utilia.ai/api/external/v1/errors/$HASH/silence" \
  -H 'Content-Type: application/json' -H 'X-Api-Key: utl_xxx' \
  -d '{ "hours": 24 }'

curl -X DELETE "https://os.utilia.ai/api/external/v1/errors/$HASH/silence" \
  -H 'X-Api-Key: utl_xxx'
```

#### JavaScript

```typescript
await sdk.errors.resolve(hash, 'Corregido en la 2.4.2');
await sdk.errors.reopen(hash);
await sdk.errors.silence(hash, 24); // de 1 a 672
await sdk.errors.unsilence(hash);
```

#### Python

```python
await sdk.errors.resolve(hash, "Corregido en la 2.4.2")
await sdk.errors.reopen(hash)
await sdk.errors.silence(hash, 24)  # de 1 a 672
await sdk.errors.unsilence(hash)
```

## Errores

| HTTP | Cuándo |
| - | - |
| `400` | Un filtro no es válido (gravedad o estado desconocidos, fecha que no es ISO 8601), `hours` está fuera de 1 a 672 o `note` pasa de 1.000 caracteres. |
| `401` | Falta la clave de API o no es válida. |
| `403` | Falta el permiso de la ruta (`INSUFFICIENT_SCOPE`), el plan no incluye Integraciones (`MODULE_NOT_ENABLED`) o, en la gestión, la persona responsable no tiene el permiso (`APP_RESPONSIBLE_PERMISSION_DENIED`) o no está activa (`APP_RESPONSIBLE_INACTIVE`). |
| `404` | No hay ningún grupo con ese hash para esta aplicación. |
| `429` | Más de 120 peticiones por minuto en la ruta. Respeta `Retry-After`. |

`APP_RESPONSIBLE_PERMISSION_DENIED` no se arregla en los permisos de la aplicación, sino en los de su persona responsable.

## Siguiente paso

- [Pestaña de errores de la aplicación](https://os.utilia.ai/docs/developers/api/errors-tab): Los estados de un grupo y lo que tu equipo hace con él desde el panel.
- [Contenido de los avisos](https://os.utilia.ai/docs/developers/webhooks/events#errores-del-sistema): Los campos de ERROR_RESOLVED, ERROR_REOPENED y ERROR_SILENCED.
