# Configurar el cliente

Opciones del cliente del SDK en JavaScript y Python: autenticación, cabeceras automáticas, qué se reintenta y cuánto espera, avisos del servidor y depuración.

El cliente del SDK se crea una vez con la dirección base de la API y una credencial, y a partir de ahí añade solo las cabeceras, reintenta lo que se puede reintentar y convierte cualquier fallo en un `UtiliaSDKError`. Esta página recoge todas sus opciones, contrastadas con el código de la versión 4.6.0 de los dos paquetes, que está sin publicar; cada opción dice desde qué versión existe.

## Opciones

Las opciones son las mismas en los dos lenguajes; cambian el estilo del nombre y la unidad del tiempo de espera.

| JavaScript | Python | Por defecto | Para qué sirve |
| - | - | - | - |
| `baseURL` | `base_url` | Obligatoria | Dirección base de la API: `https://os.utilia.ai/api`. |
| `apiKey` | `api_key` | Sin valor | Clave de la aplicación externa (`utl_...`) para integraciones de servidor a servidor. |
| `oauth` | `oauth` | Sin valor | Configuración OAuth 2.1 con PKCE, alternativa a la clave. |
| `timeout` | `timeout` | `30000` ms / `30.0` s | Tiempo máximo de espera de cada petición. En JavaScript va en milisegundos; en Python, en segundos. |
| `retryAttempts` | `retry_attempts` | `3` | Número **total** de intentos de cada petición que se puede repetir, incluido el primero. `1` desactiva los reintentos. Ver [Reintentos](#reintentos). |
| `maxRetryAfterMs` | `max_retry_after` | `60000` ms / `60.0` s | Espera máxima que el SDK acepta cuando el servidor pide esperar (4.6.0). Si pide más, no reintenta: lanza el error con lo que pidió. |
| `debug` | `debug` | `false` / `False` | Activa las trazas de depuración. |
| `onApiKeyExpires` | `on_api_key_expires` | Sin valor | Función que recibe el aviso de que la clave en uso es la anterior de una renovación (4.2.0). Ver [Aviso de que la clave en uso es la anterior](#aviso-de-que-la-clave-en-uso-es-la-anterior). |
| `onIgnoredFields` | `on_ignored_fields` | Sin valor | Función que recibe los campos que el servidor descartó sin error en una escritura (4.6.0). Ver [Campos que el servidor descarta](#campos-que-el-servidor-descarta). |

No hay más opciones: el cliente no tiene identificación automática de usuarios. Si tu aplicación trabaja con usuarios externos, identifícalos de forma explícita con [`identify`](https://os.utilia.ai/docs/developers/sdk/users) antes de operar sobre ellos.

En Python, `base_url` y `api_key` pueden ir por posición o por nombre; el resto, solo por nombre.

### 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!,
  timeout: 15000,
  retryAttempts: 5,
  debug: true,
});
```

### Python

```python
import os

from utilia_sdk import UtiliaSDK

async with UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    api_key=os.environ["UTILIA_API_KEY"],
    timeout=15.0,
    retry_attempts=5,
    debug=True,
) as sdk:
    ...
```

## Autenticación: clave u OAuth, nunca las dos

Tienes que dar una credencial y solo una. Si no das ninguna, o das las dos, el cliente no se crea: en JavaScript lanza un `UtiliaSDKError` con código `VALIDATION_ERROR` y en Python, un `ValueError`.

| | Clave de acceso | OAuth 2.1 con PKCE |
| - | - | - |
| Para qué | Integraciones de servidor a servidor | Aplicaciones que actúan en nombre de una persona |
| Qué configuras | Un valor: la clave `utl_...` | Identificador de cliente, dirección de vuelta y scopes |
| Permisos | Los scopes concedidos a la aplicación externa | Solo los scopes que la persona aceptó |
| Cabecera que envía el SDK | `X-Api-Key` | `Authorization: Bearer <token de acceso>` |
| Caducidad | No caduca | El token de acceso caduca y el SDK lo renueva solo |

Con OAuth, la configuración lleva `clientId` y `redirectUri` (obligatorios), y opcionalmente `clientSecret` (solo aplicaciones confidenciales), `scopes` (por defecto `openid`, `profile` y `email`) y `tokenStorage` para guardar los tokens donde quieras. En Python los mismos campos van en `snake_case` y puedes pasarlos como diccionario o como instancia de `OAuthConfig`.

### JavaScript

```typescript
const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  oauth: {
    clientId: 'client_xxxxxxxxxx',
    redirectUri: 'http://localhost:3000/callback',
    scopes: ['openid', 'profile', 'email'],
  },
});
```

### Python

```python
sdk = UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    oauth={
        "client_id": "client_xxxxxxxxxx",
        "redirect_uri": "http://localhost:8000/callback",
        "scopes": ["openid", "profile", "email"],
    },
)
```

El flujo completo (dirección de autorización, vuelta, almacenamiento y renovación de tokens) está en [OAuth con el SDK](https://os.utilia.ai/docs/developers/sdk/oauth).

## Cabeceras automáticas

| Cabecera | Valor | Cuándo |
| - | - | - |
| `Content-Type` | `application/json` | Siempre, salvo en las subidas de archivos, que van como `multipart/form-data`. |
| `X-Api-Key` | Tu clave | Solo con autenticación por clave. |
| `Authorization` | `Bearer <token de acceso>` | Solo con OAuth. |
| `x-request-id` | Un UUID nuevo por petición | Siempre. |
| `X-Utilia-SDK` | `utilia-sdk-js/<versión>` o `utilia-sdk-python/<versión>`, por ejemplo `utilia-sdk-js/4.6.0` | Desde la 4.2.0. En JavaScript, en las peticiones a la API, salvo las conexiones en tiempo real (`EventSource` no admite cabeceras) y las del inicio de sesión OAuth. En Python, también en las conexiones en tiempo real y las descargas de PDF. |

`X-Utilia-SDK` dice qué SDK y qué versión hace la llamada; las constantes `SDK_VERSION`, `SDK_IDENTITY` y `SDK_IDENTITY_HEADER` (en Python, `__version__`, `SDK_IDENTITY` y `SDK_IDENTITY_HEADER`) exponen esos valores.

El `x-request-id` sirve para relacionar un fallo de tu lado con los registros del servidor. Cuando la petición falla, el error lo trae en `requestId` (JavaScript) o `request_id` (Python): en Python es el valor que devuelve el servidor en su respuesta; en JavaScript, ese mismo o, si el servidor no lo devuelve, el que generó el SDK.

## Reintentos

El SDK reintenta solo lo que tiene sentido repetir:

- **Errores de red**: sin respuesta del servidor (tiempo agotado, conexión rechazada o cortada).
- **HTTP 429**: límite de peticiones superado. También un 403 cuyo mensaje dice «rate limit», que el SDK trata igual.
- **HTTP 5xx**: errores del servidor.

No reintenta los 400 y 422 (datos no válidos), 401 (credencial no válida), 403 (acceso denegado o [bloqueo por plan](https://os.utilia.ai/docs/developers/sdk/plan-errors)), 404 (no encontrado) ni 409 (choca con el estado del recurso): repetirlos daría el mismo resultado.

### Qué peticiones se repiten

Desde la 4.6.0, solo se repite lo que no puede duplicar nada:

- **Sí**: las lecturas, `PUT`, `PATCH` y `DELETE`; un `POST` con clave de idempotencia (el campo `idempotencyKey` del cuerpo y, en JavaScript, también la cabecera `Idempotency-Key`); y los `POST` que solo consultan, como `invoices.validate`, `users.identify`, el pase de conexión, `diagnostics.checkRequest`, `diagnostics.run` y el carril MCP.
- **No**: un `POST` sin clave de idempotencia (tickets, mensajes, clientes, leads, conversión de un lead, canales, agentes, subidas de archivos e IA). Si el corte llega después de que el servidor registre la petición, repetirla la duplicaría: si falla por la red, comprueba si se creó antes de volver a lanzarla.

Hasta la 4.5, esas altas se repetían como cualquier otra petición. Si tu código daba por hecho que el SDK las repetía, ahora te llega el error a la primera.

> **Nota: Algunas escrituras no se repiten nunca**
>
> Las operaciones que no se pueden repetir sin riesgo de duplicar algo hacen un solo intento aunque lleven clave de idempotencia y configures más: emitir o anular una factura, enviarla, cobrarla sin el usuario delante, registrar un pago o una devolución, programar o reintentar un cobro, registrar una autorización de cobro y algunas operaciones de suscripciones. Si fallan por la red, repítelas tú con la misma clave de idempotencia, o comprueba el estado antes de volver a lanzarlas. El cobro con el usuario delante (`payInvoice`) sí sigue la política general de reintentos.

### Cuánto espera entre intentos

Si el servidor dice cuánto esperar, el SDK le hace caso: en un 429, o en un 503 que la traiga, espera lo que pide la cabecera `Retry-After`. `X-Utilia-Quota-Reset` solo cuenta como espera en el 429 del cupo por hora agotado (`RATE_LIMIT_EXCEEDED` sin llamadas restantes) que no traiga `Retry-After`: el servidor manda esa cabecera en todas las respuestas, y en un 5xx o en otro 429 no tiene nada que ver con cuánto esperar, así que esos se repiten con la espera creciente de siempre. Si la espera pasa de `maxRetryAfterMs` (`max_retry_after`), que por defecto es un minuto, no espera ni reintenta: lanza el error con `retryAfterMs` (`retry_after`, en segundos), para que decidas tú si lo dejas para más tarde. Es lo que pasa cuando se agota el cupo por hora de la clave.

Si el servidor no dice nada, espera cada vez el doble (0,5 s, 1 s, 2 s, 4 s…). Con el valor por defecto de `retryAttempts`, `3`, hace un intento y hasta dos reintentos:

**Reintentos sin indicación del servidor**

1. **0 s**: Intento 1. Falla por la red o con un 5xx. Espera 0,5 s hasta el siguiente.
2. **0,5 s**: Intento 2. Vuelve a fallar por lo mismo. Espera 1 s hasta el siguiente.
3. **1,5 s**: Intento 3. Si también falla, el SDK lanza el UtiliaSDKError del último intento.

_Con retryAttempts igual a 3, sin contar lo que tarda cada petición. Con Retry-After, la espera es la que pide el servidor._

Con OAuth, un 401 provoca además una renovación automática del token y un nuevo intento con el token renovado. Si la renovación falla, el error llega tal cual. Desde la 4.2.0 hay una excepción: un 401 `OAUTH_BEARER_NOT_ALLOWED` no renueva el token ni repite la petición, porque esa ruta exige la sesión de una persona y renovar no cambia nada.

## Depuración

Con `debug` activado, el SDK deja una traza por petición y otra por respuesta:

- **JavaScript** escribe en la consola con el prefijo `[UtiliaSDK]`: `Petición: POST /external/v1/tickets`, `Respuesta: 201 /external/v1/tickets` y, si reintenta, `Reintento 1/2, esperando 500ms`.
- **Python** usa el registro estándar con el nombre `utilia_sdk` y nivel `DEBUG`: `POST /external/v1/tickets (intento 1)` y `Respuesta: 201 /external/v1/tickets`. No imprime nada hasta que configuras el registro de tu aplicación, por ejemplo con `logging.getLogger("utilia_sdk").setLevel(logging.DEBUG)`.

No dejes la depuración activada en producción: las trazas muestran las rutas que llamas.

## Aviso de que la clave en uso es la anterior

Cuando alguien renueva la clave de tu aplicación con un periodo de transición, la anterior sigue valiendo unos días y cada respuesta a una petición hecha con ella trae la cabecera `X-Utilia-Api-Key-Expires` con la fecha en que dejará de valer. Desde la 4.2.0, el SDK la lee por ti:

- Si configuras `onApiKeyExpires` (`on_api_key_expires` en Python), la llama **una vez por fecha** con un aviso que trae `expiresAt` (`expires_at`).
- Si no la configuras, deja un único aviso por fecha: en JavaScript, en la consola; en Python, en el registro `utilia_sdk` con nivel `WARNING`.
- `sdk.apiKeyExpiresAt` (`sdk.api_key_expires_at`) devuelve esa fecha, o `null` (`None`) si el servidor no ha avisado.

El aviso es solo informativo: nunca cambia el resultado de la petición, y si tu función falla, el SDK lo ignora. Úsalo para saber que algún servidor tuyo todavía no tiene la clave nueva.

### JavaScript

```typescript
const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
  onApiKeyExpires: ({ expiresAt }) => {
    alertas.enviar(`La clave de UTILIA OS de este servidor deja de valer el ${expiresAt}`);
  },
});
```

### Python

```python
import logging
import os

from utilia_sdk import ApiKeyExpiryNotice, UtiliaSDK


def avisar(notice: ApiKeyExpiryNotice) -> None:
    logging.error("La clave de UTILIA OS deja de valer el %s", notice.expires_at)


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

Cómo se renueva la clave y cuánto dura la transición está en [Autenticación con clave de acceso](https://os.utilia.ai/docs/developers/getting-started/authentication#de-dónde-sale-la-clave).

## Campos que el servidor descarta

Una escritura puede salir bien y aun así no guardar un dato: si mandas un campo que la ruta no admite, casi siempre un nombre mal escrito, el servidor lo descarta sin error y lo dice en la cabecera `X-Utilia-Ignored-Fields`. Desde la 4.6.0, el SDK la lee por ti:

- Si configuras `onIgnoredFields` (`on_ignored_fields`), la llama **una vez por método, ruta y campos** con un aviso que trae `method`, `path`, `fields` (los nombres, nunca los valores) y `requestId` (`request_id`).
- Si no la configuras, deja un aviso: en JavaScript, en la consola; en Python, en el registro `utilia_sdk` con nivel `WARNING`.

Como el otro aviso, nunca cambia el resultado de la petición. Para saber antes de enviarla qué campos se descartarían, usa [`diagnostics.checkRequest`](https://os.utilia.ai/docs/developers/sdk/reference#diagnostics).

### JavaScript

```typescript
const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
  onIgnoredFields: ({ method, path, fields, requestId }) => {
    registro.warn(`${method} ${path} descartó ${fields.join(', ')} (petición ${requestId})`);
  },
});
```

### Python

```python
import logging
import os

from utilia_sdk import IgnoredFieldsNotice, UtiliaSDK


def avisar(notice: IgnoredFieldsNotice) -> None:
    logging.warning(
        "%s %s descartó %s (petición %s)",
        notice.method, notice.path, ", ".join(notice.fields), notice.request_id,
    )


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

## Conexiones en tiempo real

Las funciones que reciben eventos en tiempo real (por ejemplo, el seguimiento de [sugerencias con IA](https://os.utilia.ai/docs/developers/sdk/ai-suggestions)) usan Server-Sent Events. Desde la 4.2.0, la clave no viaja nunca en la dirección de la conexión: en JavaScript, como `EventSource` no admite cabeceras propias, el SDK pide antes un pase de conexión de un solo uso y abre la conexión con él; en Python, la clave va en la cabecera `X-Api-Key`. Lo hace por dentro: no tienes que construir esa dirección tú. Si necesitas abrir una conexión por tu cuenta, pide el pase con [`createStreamToken`](https://os.utilia.ai/docs/developers/sdk/tickets#abrir-la-conexión-por-tu-cuenta).

Si tu código usaba `buildSseUrl` o `buildSseUrlAsync` del cliente de JavaScript, ten en cuenta que están en desuso y ya no ponen la clave en ninguna dirección: su sustituto es `buildUrl(path)`, que devuelve la dirección sin credenciales. Para las conexiones de tickets, usa el `url` del pase.

## Diferencias entre los dos lenguajes

| Aspecto | JavaScript | Python |
| - | - | - |
| Nombres | `camelCase` (`baseURL`, `apiKey`) | `snake_case` (`base_url`, `api_key`) |
| Tiempo de espera | Milisegundos | Segundos |
| Cliente HTTP | `axios` | `httpx` |
| Modelos | Interfaces de TypeScript | Modelos de Pydantic |
| Modo síncrono | No aplica | `UtiliaSDKSync` |
| Cerrar la sesión | No hace falta | `async with` / `with`, o `close()` |
| Comprobaciones del error | Métodos: `error.isRateLimited()` | Propiedades: `error.is_rate_limited` |

## Siguiente paso

- [Gestionar errores](https://os.utilia.ai/docs/developers/sdk/error-handling): Qué trae un UtiliaSDKError, cuándo reintentar y cómo distinguir un bloqueo de plan.
- [OAuth con el SDK](https://os.utilia.ai/docs/developers/sdk/oauth): Actuar en nombre de una persona: autorización, tokens y renovación.
