# Errores frecuentes al integrar

Los diez errores que más se repiten en las primeras integraciones con la API externa de UTILIA OS y cómo evitarlos.

Esta página recoge los errores que más se repiten en las primeras integraciones, cada uno con su forma correcta. Léela antes de llevar tu integración a producción.

## 1. No mandes `userId` en el cuerpo cuando el endpoint lo espera en la consulta

Muchos endpoints de tickets, facturas y tarjetas esperan `userId` en la consulta, no en el cuerpo. Mandarlo mal no da un error claro: la API descarta el campo que no espera y la petición falla después con `USER_NOT_FOUND`. La única pista es la cabecera `X-Utilia-Ignored-Fields` de la respuesta, que en las escrituras nombra los campos descartados; mírala en tus registros o comprueba la petición antes con `POST /api/external/v1/diagnostics/check-request` ([Comprobar una petición sin ejecutarla](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla)).

**Dónde aplica:**

- `GET /tickets/{id}` (query)
- `GET /tickets` (query)
- `GET /tickets/unread-count` (query)
- `POST /tickets/{id}/messages` (en la consulta, no en el cuerpo)
- `PATCH /tickets/{id}/close` (query)
- `PATCH /tickets/{id}/reopen` (query)
- `GET /invoices/{id}` (query)
- `GET /invoices/{id}/pdf` (query)
- `DELETE /payment-methods/{pmId}` (query)

**Mal:**

```http
POST /api/external/v1/tickets/abc-123/messages
Content-Type: application/json

{ "userId": "usr-4821", "content": "..." }
```

**Bien:**

```http
POST /api/external/v1/tickets/abc-123/messages?userId=usr-4821
Content-Type: application/json

{ "content": "..." }
```

La lista completa está en [Dónde va cada parámetro](https://os.utilia.ai/docs/developers/getting-started/parameters).

## 2. No mandes campos extra dentro de `context`

`context` (en `POST /tickets`) admite solo `url`, `appVersion`, `browserInfo` y `customData`. Cualquier otro campo **se descarta sin error**: crees que envías datos que en realidad se pierden, y solo la cabecera `X-Utilia-Ignored-Fields` de la respuesta lo dice (por ejemplo, `context.plan`). Si necesitas más, mételos dentro de `context.customData`.

**Mal:**

```json
{
  "user": { "externalId": "usr-1" },
  "title": "...",
  "description": "...",
  "context": {
    "url": "https://miapp.com",
    "userPlan": "premium",
    "abVariant": "B"
  }
}
```

`userPlan` y `abVariant` no son campos de `context`: la petición se acepta, pero nunca llegan al ticket.

**Bien:**

```json
{
  "user": { "externalId": "usr-1" },
  "title": "...",
  "description": "...",
  "context": {
    "url": "https://miapp.com",
    "customData": {
      "userPlan": "premium",
      "abVariant": "B"
    }
  }
}
```

Si los datos son del usuario y no del momento del ticket, usa `user.metadata`, que se guarda con el usuario, en lugar de `context.customData`, que solo vale para ese ticket.

## 3. No pintes el objeto de error en tu interfaz

Cuando un endpoint falla, la respuesta es un objeto JSON con la forma:

```json
{
  "statusCode": 400,
  "message": "Tipo de archivo no permitido: image/bmp...",
  "error": "BAD_REQUEST",
  "errorCode": "FILE_INVALID_MIME",
  "timestamp": "2026-02-10T14:23:45.123Z",
  "requestId": "550e8400-..."
}
```

Si pasas el objeto entero como hijo en JSX (`<div>{error.response.data}</div>`) o a `toast.error(error.response.data)`, React lanza «Objects are not valid as a React child» y la pantalla se rompe.

**Mal:**

```tsx
toast.error(error.response.data); // React falla y la pantalla se rompe
```

**Bien:**

```tsx
toast.error(error.response.data?.message ?? 'Error desconocido');
// y guarda error.response.data?.requestId para soporte
```

Usa `errorCode` para decidir en tu código y `message` para el texto que ve la persona.

## 4. No confundas `/errors/report` con `/tickets/client-errors`

Son dos endpoints con propósitos distintos, y se confunden porque los dos reciben errores.

| Endpoint | Para qué sirve |
| - | - |
| `POST /errors/report` y `POST /errors/batch` | Los **errores de tu aplicación**, en el navegador o en tu servidor. Aceptan `module`, `file`, `line`, `function`, `context`, `release`, `fingerprint` y `tags`, agrupan los iguales y responden por cada error. Es lo que usan `install` y `capture` del SDK. |
| `POST /tickets/client-errors` | La telemetría ligera del **widget** y de `ai.reportError`. Acepta `code`, `component`, `url` y `userAgent`, en lotes de 10, y solo responde con recuentos. |

Si mandas un error de tu aplicación a `/tickets/client-errors`, pierdes `module`, `file`, `line` y `context`. Al revés, si mandas un fallo del widget a `/errors/report`, `component`, `url` y `userAgent` se descartan sin aviso; ponlos en `context`.

Lo más sencillo es no llamar a ninguno a mano: `sdk.errors.install()` y `sdk.errors.capture(error)` van al registro de errores, envían en lote y nunca lanzan. Detalle en [Capturar los errores de tu aplicación](https://os.utilia.ai/docs/developers/sdk/error-reporting) y [Telemetría ligera del widget y del SDK](https://os.utilia.ai/docs/developers/api/client-errors).

## 5. No mandes cuerpo a `PATCH /close` ni a `/reopen`

Estos endpoints solo usan el identificador de la ruta (`{id}`) y `userId` en la consulta. Si envías cuerpo, se ignora: los datos no se guardan y tampoco hay error.

Si quieres dejar el motivo del cierre, envíalo como mensaje antes de cerrar (ver el [ejemplo](https://os.utilia.ai/docs/developers/getting-started/parameters#ejemplo-2-cerrar-un-ticket)).

## 6. No ignores `USER_NOT_FOUND`

`404 USER_NOT_FOUND` significa que el endpoint necesita un usuario externo ya identificado y no lo está. Pasa en casi todos los endpoints que reciben `userId` suelto (no un objeto `user`).

**Mal:** capturar el error y mostrar «Algo salió mal» sin hacer nada más.

**Bien:**

- Llama a `POST /users/identify` antes del primer endpoint que lo necesite y guarda el `externalId` en tu sistema.
- Con el SDK de JavaScript o Python, identifica al usuario con `sdk.users.identify(...)` antes de operar sobre él. El SDK no lo hace por su cuenta:

```ts
const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: 'utl_xxx',
});

await sdk.users.identify({
  externalId: 'user_42',
  email: 'ada@example.com',
  name: 'Ada Lovelace',
});
```

Más en la configuración del [SDK de JavaScript](https://os.utilia.ai/docs/developers/sdk/configuration) y del [SDK de Python](https://os.utilia.ai/docs/developers/sdk/configuration).

## 7. No reintentes en errores 4xx

Reintenta solo en `429`, `5xx` o fallos de red pasajeros (tiempo agotado, conexión cortada), o cuando el cuerpo del error trae `retryable: true` (por ejemplo, un `409` porque la factura todavía se está emitiendo): entonces espera `retryAfterSeconds` y repite la misma petición. Reintentar un `400`, `401`, `403` o `404` no arregla nada y gasta tu cupo de peticiones, y un error con `retryable: false` tampoco se arregla repitiéndolo.

Usa espera exponencial (1, 2, 4 y 8 segundos) con algo de azar y un tope de cuatro intentos. Ante un `5xx` o un corte de red en una escritura, mira antes si lleva `idempotencyKey`: con ella, repite con la misma; sin ella (abrir un ticket, identificar a un usuario), comprueba si la operación llegó a hacerse antes de repetirla. Qué rutas la admiten está en [Repetir sin duplicar](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#repetir-sin-duplicar-idempotencykey).

## 8. No pongas la clave de servidor en el navegador

La clave de una aplicación con permisos de servidor (facturas, cobros, CRM) es secreta: si llamas con ella desde el navegador, cualquiera puede verla en las herramientas de desarrollo. Llama desde tu servidor o pasa por un intermediario tuyo que añada la clave. Si tu web necesita llamar a la API desde el navegador, como hace el widget de soporte, usa otra aplicación, con dominios autorizados y solo los permisos del navegador: 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).

## 9. Envía tu propio `X-Request-Id`

La cabecera `X-Request-Id` une tu petición con los registros de UTILIA OS. Si no la envías, el servidor genera una y te la devuelve; si la envías tú, puedes seguir la misma petición en tus registros desde el principio. Genera un identificador por petición, guárdalo en tus registros y dáselo a soporte cuando algo falle.

```http
X-Api-Key: utl_xxx
X-Request-Id: 7c2d5b30-f8a1-4d6e-9c3b-1234567890ab
```

## 10. No decidas con el campo `error`

`error` (`"BAD_REQUEST"`, `"NOT_FOUND"`...) solo repite el estado HTTP. Para decidir en tu código, usa `errorCode` (`"USER_NOT_FOUND"`, `"TICKET_CANNOT_REOPEN"`...). El catálogo completo está en [Errores, identificador de petición y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries).
