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/connectRequiere 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ó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. |
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).