Emisso Connect

SDK de TypeScript

En camino a npm. El contrato de @emisso/connect, el cliente tipado y sus reglas de reintento, documentados para cuando se publique.

En camino: el paquete todavía no está publicado en npm

pnpm add @emisso/connect aún no funciona fuera de este repositorio. Hoy la integración real es HTTP directo: los mismos endpoints de tu primera llamada y de la API de control, con Authorization: Bearer connect_sk_…. Esta página documenta el contrato del SDK tal como existe en el código, para que la migración sea directa cuando se publique.

@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.

Instalación

pnpm add @emisso/connect

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

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ónTipoDefaultNotas
apiKeystringTu clave connect_sk_…. Provee esta o apiKeyProvider.
apiKeyProvider() => string | Promise<string>Fuente asíncrona de la clave (por ejemplo un vault).
baseUrlstringhttps://connect.emisso.ai/api/v1Se preserva el subpath; se recorta el / final.
maxAttemptsnumber3Intentos 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).
fetchtypeof fetchglobalThis.fetchReemplaza la implementación de fetch.
clientTraceIdstringTrace id por defecto para toda llamada; se sobreescribe por llamada desde CallOptions. Ver Trazabilidad.

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-08", valor: 39487.23, 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.

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? }. connectionId viaja como el header X-Connect-Connection; signal es un AbortSignal estándar para cancelar la llamada; clientTraceId se explica abajo.

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.

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.

Un Idempotency-Key viaja en cada llamada como preparación a futuro (hoy es un no-op en el servidor) y se mantiene estable a través del reintento por refresh OAuth: cuando el servidor active el almacén de idempotencia, un POST reintentado tras renovar el token deduplicará contra su primer intento en vez de ejecutarse dos veces.

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).

On this page