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.
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 y de la
API de 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.
@emisso/connect es el SDK oficial en TypeScript para Connect: habla REST por debajo (los mismos endpoints de tu 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.
Instalación
pnpm add @emisso/connectRequiere Node 20 o superior (usa fetch nativo; puedes reemplazarlo con opts.fetch).
Qué cubre la 0.1.0
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 |
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
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.
createClient(options)
| 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. | |
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. |
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
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:
// 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.
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
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.
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.
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.
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:
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.
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
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:
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
Desde la 0.2.0, cada petición lleva una cabecera que dice qué paquete la armó:
x-connect-client: @emisso/connect/0.2.0Es 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:
const connect = createClient({ apiKey, app: { name: "mi-erp", version: "3.1.0" } });
// x-connect-client: mi-erp/3.1.0 @emisso/connect/0.2.0Nuestro 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
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:
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 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
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.
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.
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).
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 y en Emitir en el SII.
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.
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).