# SDK de TypeScript

> El cliente tipado de @emisso/connect: instalación, accessor por tool, selección de conexión, idempotencia, reglas de reintento y qué cubre la 0.1.0.



<Callout type="warn" title="Revisa qué versión te instaló npm antes de apoyarte en esta página">
  Hasta el 2026-09-10 la única versión en npm era la **0.1.0**, del 2026-08-25. Instala y funciona, pero
  su árbol tipado se generó en agosto y quedó atrás respecto del registro, y su `CallOptions`
  **no expone `idempotencyKey`**. El detalle de qué le falta está más abajo, en «Qué cubre la 0.1.0»,
  medido el 2026-09-10.

  Comprueba cuál tienes con `npm view @emisso/connect version`: de la 0.2.0 en adelante esa sección ya no
  te aplica y el resto de la página describe lo que instalaste.

  El resto de esta página describe la superficie completa, la que vive en el repositorio. Si tu versión
  no trae algo que necesitas, HTTP directo llega a todo el catálogo sin instalar nada: los mismos
  endpoints de [tu primera llamada](/docs/empezar/primera-llamada) y de la
  [API de control](/docs/api-control), con `Authorization: Bearer connect_sk_…`. El SDK habla exactamente
  eso por debajo, así que moverse de uno al otro no cambia ni el modelo ni las credenciales.
</Callout>

`@emisso/connect` es el SDK oficial en TypeScript para Connect: habla REST por debajo (los mismos endpoints de [tu primera llamada](/docs/empezar/primera-llamada)) y expone encima un **accessor tipado**, generado desde el registro de tools, sin escribir a mano los tipos de cada `connector.resource.verb`.

Para ver un programa completo de punta a punta, con una demo animada del editor y la terminal, mira [Consultar con el SDK](/docs/como-consultar/sdk).

## Instalación [#instalación]

```bash
pnpm add @emisso/connect
```

Requiere Node 20 o superior (usa `fetch` nativo; puedes reemplazarlo con `opts.fetch`).

## Qué cubre la 0.1.0 [#qué-cubre-la-010]

Contrastado contra npm y contra producción el **2026-09-10**. Instalada en limpio, la 0.1.0 hace su
trabajo: apunta a `https://connect.emisso.ai/api/v1`, manda `X-Connect-Connection` cuando pasas
`connectionId`, y ante una clave que no sirve devuelve un `ConnectError` con `code: "unauthorized"`,
`status: 401` y su `requestId`, en menos de un segundo.

Lo que le falta frente a lo que documenta esta página:

| Diferencia                                                                                              | Qué implica al escribir código                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Su árbol tipado conoce **54 ids de tool**; ese día el registro tenía **70** (hoy son más). Coinciden 52 | Las 18 que faltan no están en el árbol: `connect.tools.santander_empresas` y `connect.tools.sii.boleta` son `undefined` en tiempo de ejecución, no un error del compilador. Son Santander Empresas (4), la emisión y la previsualización del SII y de e-Boleta (4), 8 de Notta, `previred.nominas.consultar` y `bch_empresas.movimientos_tarjetas.consultar` |
| Tipa dos ids que el catálogo ya no tiene                                                                | `notta.dte.consultar` y `notta.dte.descargar` compilan y el gateway los rechaza con `tool_not_found`                                                                                                                                                                                                                                                         |
| `CallOptions` es `{ signal?, connectionId?, clientTraceId? }`                                           | Sin `idempotencyKey`, la clave la acuña el SDK por llamada y no la puedes fijar. Eso deja fuera la emisión ante el SII; ver [Idempotencia](#idempotencia)                                                                                                                                                                                                    |

Mientras sigas en esa versión, el camino que funciona para cualquiera de esos tres casos es REST, con
el mismo modelo y la misma credencial. El escape hatch `connect.tools.execute(id, input, opts)` alcanza
los ids que no están en el árbol, pero devuelve `unknown` y tampoco resuelve la falta de
`idempotencyKey`.

## Primeros pasos [#primeros-pasos]

```ts
import { createClient } from "@emisso/connect";

const connect = createClient({ apiKey: "connect_sk_..." });

const now = await connect.tools.core.timestamp.now({ timezone: "America/Santiago" });
// { iso: "2026-07-21T15:04:00.000Z", unix: 1784646240, timezone: "America/Santiago" }
```

La API key es la misma que creas desde `/api-keys` en el dashboard: el SDK no introduce un modelo de credenciales distinto. El detalle de autenticación y scopes está en [Autenticación](/docs/empezar/autenticacion).

## `createClient(options)` [#createclientoptions]

| Opción           | Tipo                                 | Default                            | Notas                                                                                                                                           |
| ---------------- | ------------------------------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`         | `string`                             |                                    | Tu clave `connect_sk_…`. Provee esta **o** `apiKeyProvider`.                                                                                    |
| `apiKeyProvider` | `() => string \| Promise<string>`    |                                    | Fuente asíncrona de la clave (por ejemplo un vault).                                                                                            |
| `baseUrl`        | `string`                             | `https://connect.emisso.ai/api/v1` | Se preserva el subpath; se recorta el `/` final.                                                                                                |
| `maxAttempts`    | `number`                             | `3`                                | Intentos totales por llamada, el primero incluido. Aplica **solo a GET**.                                                                       |
| `onUnauthorized` | `() => Promise<string \| null>`      |                                    | Seam de refresh OAuth: ante un 401 se llama una vez para obtener un token nuevo y se reintenta la llamada (cualquier verbo).                    |
| `fetch`          | `typeof fetch`                       | `globalThis.fetch`                 | Reemplaza la implementación de fetch.                                                                                                           |
| `clientTraceId`  | `string`                             |                                    | Trace id por defecto para toda llamada; se sobreescribe por llamada desde `CallOptions`. Ver [Trazabilidad](#trazabilidad-clienttraceid).       |
| `app`            | `{ name: string; version?: string }` |                                    | Nombre de tu aplicación, para que tus llamadas no se cuenten como del SDK a secas. Ver [Cómo se identifica el SDK](#cómo-se-identifica-el-sdk). |

Los errores de configuración (falta `fetch`, falta `apiKey`/`apiKeyProvider`) se lanzan de forma síncrona desde el propio `createClient(...)`, antes de cualquier llamada.

## Accessor tipado [#accessor-tipado]

Cada tool del registro queda disponible como `connect.tools.<connector>.<resource>.<verbo>(input?, opts?)`, con el tipo de `input`/`output` derivado del registro. No hay `any` en el camino:

```ts
// connect.tools.<connector>.<resource>.<verbo>(input?, opts?)
const uf = await connect.tools.indicadores.valor.actual({ codigo: "UF" });
// { codigo: "UF", fecha: "2026-08-10", valor: 40846.11, unidad: "CLP", antiguedadDias: -1 }

// .withResponse(...) devuelve el envelope completo { data, requestId, meta, pagination }
const res = await connect.tools.core.timestamp.now.withResponse();
console.log(res.requestId, res.data);

// escape hatch genérico, útil si el id de la tool es dinámico
const data = await connect.tools.execute("core.timestamp.now", {});
```

La llamada por defecto (sin `.withResponse`) devuelve directamente el `data` de salida. Para el `request_id`, `meta` o `pagination` completos, usa `.withResponse(...)`. El catálogo completo de tools, con la ficha de cada una, vive en [la referencia](/docs/referencia).

El árbol de la 0.1.0 cubre 52 de esas tools; la referencia lista todas las del catálogo vigente, que
son más. Si un conector reciente te aparece como `undefined`, no es un error de instalación: revisa tu
versión y la tabla de arriba.

## Selección de conexión [#selección-de-conexión]

Toda tool de un conector **con conexión** (el SII, los bancos) exige `connectionId` en `opts`. Siempre, incluso si tienes una sola conexión de ese sistema: no hay resolución implícita.

```ts
await connect.tools.sii.conexion.sincronizar(
  { periodo: "2026-06", alcances: ["rcv"] },
  { connectionId: "conn_..." },
);
```

Sin él la llamada falla con `400 validation_error` y un `suggestedFix` que dice de dónde sacar el id.

<Callout type="info" title="Por qué no se elige sola">
  La conexión **es la empresa**: dos conexiones de un banco son dos RUT distintos. Una elección implícita que hoy acierta porque hay una sola, mañana (al conectar la segunda empresa) acierta distinto sin que nadie haya cambiado una línea. Es exactamente el tipo de cambio silencioso que no queremos en plata ajena.
</Callout>

Los conectores **sin** conexión quedan exentos, porque no hay nada que elegir: `core`, `indicadores` y `conexiones` no tienen fila en `connections`. `conexiones.enlace.crear` es el caso que lo hace obvio: todavía no existe la conexión que crearía.

Para obtener los `conn_…` de tu organización, pregúntaselos al propio gateway:

```ts
const { conexiones } = await connect.tools.conexiones.estado.consultar({});
for (const cx of conexiones) {
  console.log(cx.id, cx.sistema, cx.nombre, cx.alcances, cx.datosListos);
}
```

`datosListos` responde «¿ya puedo leer?»: un listado vacío no es lo mismo que «no hay nada», puede ser que todavía no sincronizó. El porqué de esa separación está en [sincronizar y consultar](/docs/conceptos/sincronizar-consultar).

`opts: CallOptions = { signal?, connectionId?, clientTraceId?, idempotencyKey? }`. `connectionId` viaja como el header `X-Connect-Connection`; `signal` es un `AbortSignal` estándar para cancelar la llamada; `clientTraceId` y `idempotencyKey` se explican abajo. El campo `idempotencyKey` no existe en la 0.1.0.

## Trazabilidad: `clientTraceId` [#trazabilidad-clienttraceid]

Una API key autentica como **un** agente. Si tu aplicación sirve a varias personas con la misma clave, la bitácora las ve a todas como el mismo actor y «quién pidió esto» queda sin respuesta. `clientTraceId` es la vía para decirlo:

```ts
const connect = createClient({ apiKey, clientTraceId: "miapp:tenant-42" });

// o por llamada, que gana sobre el del cliente
await connect.tools.sii.rcv.consultar({ limit: 10 }, { connectionId: "conn_...", clientTraceId: "miapp:tenant-42:hilo-9" });
```

Viaja como el header `x-client-trace-id` y se guarda en `bitacora_execution.client_trace_id`, en su propia columna y explícitamente no confiable: nunca se confunde con el `request_id` que emite el servidor. El SDK lo sanea antes de mandarlo: descarta caracteres de control y recorta a 200 caracteres, para que un valor construido concatenando texto de un tercero no pueda inyectar cabeceras.

## Cómo se identifica el SDK [#cómo-se-identifica-el-sdk]

Desde la `0.2.0`, cada petición lleva una cabecera que dice qué paquete la armó:

```http
x-connect-client: @emisso/connect/0.2.0
```

Es todo lo que manda: el nombre del paquete y su versión, tomada de su propio `package.json` en el build. No lleva ningún dato de tu aplicación, de tus usuarios ni de tu clave, y no hay forma de que los lleve: el valor no depende de la llamada.

Del lado del servidor se guarda en `bitacora_execution.client_declared`, en su propia columna y explícitamente no confiable, igual que `client_trace_id`. Sirve para saber cuánta gente usa el SDK y con qué versión, y para nada más: no autoriza, no atribuye y no cobra. Si la cabecera no llega o no se entiende, la columna queda nula y tu llamada sigue exactamente igual.

Si tu producto envuelve al SDK, puedes decir quién eres:

```ts
const connect = createClient({ apiKey, app: { name: "mi-erp", version: "3.1.0" } });
// x-connect-client: mi-erp/3.1.0 @emisso/connect/0.2.0
```

Nuestro propio CLI hace exactamente eso (`emisso-cli/…`), y por eso sus llamadas no se confunden con las de quien instaló el paquete. El nombre acepta letras, números, punto, guion, guion bajo y la barra de un scope de npm; lo que no calce se descarta y la petición sale igual, con el SDK identificándose solo.

## Errores [#errores]

`ConnectError` se lanza para fallas a nivel del gateway (una respuesta HTTP no-ok) y para errores de configuración del cliente. Los de configuración salen del propio `createClient(...)`, así que el `try/catch` de abajo, alrededor de una llamada a una tool, no los cubre:

```ts
import { ConnectError } from "@emisso/connect";

try {
  await connect.tools.core.timestamp.now();
} catch (e) {
  if (e instanceof ConnectError) {
    console.error(e.code, e.status, e.requestId, e.suggestedFix);
  }
}
```

`code` es un valor del [catálogo de errores](/docs/operar/errores) compartido (`@emisso/contracts`), o `"transport_error"` cuando el cuerpo del error no trae un `error.code` parseable. Todo error del gateway trae `requestId` y un `suggestedFix` pensado para que un agente LLM pueda actuar sobre él directamente. Cuando el servidor adjunta detalle estructurado (por ejemplo los issues campo a campo de un `validation_error`), llega en `e.details`.

Una falla de red que nunca llega al gateway (DNS, conexión rechazada, una request abortada) se propaga como el rechazo nativo de `fetch`; **no** se envuelve en un `ConnectError`.

## Reintentos [#reintentos]

Los `GET` se reintentan hasta `maxAttempts` cuando el error es retryable según el catálogo. Si la respuesta trae `Retry-After` (en segundos o como fecha HTTP), se respeta; si no, el backoff es exponencial, con un techo de 8 segundos entre intentos.

<Callout type="warn" title="POST/execute nunca se reintenta solo">
  Toda llamada a una tool es un `POST` (`execute`), y el SDK **nunca la reintenta automáticamente**: una acción regulada (un giro bancario, una declaración SII) no puede arriesgarse a un doble filing. La única excepción es el refresh OAuth: un único 401→refresh→reintento vía `onUnauthorized`, para cualquier verbo, fuera del presupuesto de `maxAttempts`.
</Callout>

## Idempotencia [#idempotencia]

Un `Idempotency-Key` viaja en **cada** llamada y se mantiene **estable** a través del reintento por refresh OAuth: un POST reintentado tras renovar el token deduplica contra su primer intento en vez de ejecutarse dos veces.

Del lado del servidor la clave no es decorativa: el gateway consulta su almacén de idempotencia para las tools marcadas como destructivas (las que tienen un efecto que no se puede deshacer) y puede responder `idempotency_conflict` (la misma clave con un input distinto) o `idempotency_in_progress` (otro intento con esa clave todavía corriendo). Ninguno de los dos se arregla acuñando una clave nueva para la misma operación lógica; el `suggestedFix` de cada error dice qué hacer.

Por defecto el SDK acuña la clave por ti, una por llamada. **Las emisiones ante el SII son la excepción y exigen que la pongas tú:** `sii.boleta.emitir` (e-Boleta), `sii.dte.emitir` y `sii.anulacion.emitir` (Facturador Gratuito).

```ts
await connect.tools.sii.dte.emitir(
  { previewRef },
  { connectionId: "conn_...", idempotencyKey: crypto.randomUUID() },
);
```

Tiene que ser una UUID v4 canónica en minúsculas, y **la misma** mientras reintentes esa emisión: es lo que distingue «reintentar el documento que ya mandé» de «emitir otro». Sin ella la llamada falla antes de salir del cliente, con `idempotency_key_missing`. Cuáles son las tools que la exigen no lo decide el SDK: lo declara el registro, y el gateway aplica el mismo control en su puerta, así que una llamada que el SDK deja pasar sin clave tampoco la necesita del otro lado. Los pasos completos de cada flujo, con quién confirma la preparación y cómo se consulta el intento guardado, están en [Boletas 39 y 41](/docs/emitir/boletas) y en [Emitir en el SII](/docs/emitir).

<Callout type="warn" title="Ese bloque no corre contra la 0.1.0">
  La 0.1.0 no tiene `sii.boleta`, `sii.dte.emitir` ni `sii.anulacion.emitir` en su árbol, ni `idempotencyKey` en `CallOptions`.
  Si es la versión que tienes instalada, emite por REST:
  `POST /api/v1/tools/sii.dte.emitir/execute` (o `sii.anulacion.emitir`, o `sii.boleta.emitir`), con la UUID v4 en el header
  `Idempotency-Key`.
</Callout>

## Tipos exportados [#tipos-exportados]

Además de `createClient` y `ConnectError`, el paquete exporta `ClientOptions`, `ConnectClient` (el tipo del cliente ya construido, útil para pasarlo entre funciones) y `ConnectToolIO` (el mapa `id → { input, output }` de todas las tools, el mismo del que se deriva el accessor).
