SDK TypeScript
@cierrelisto/sdk es una capa pequeña sobre el API HTTP. Comparte los tipos y schemas Zod que
generan la referencia OpenAPI; no mantiene una segunda definición de factura.
Cuándo usarlo
Sección titulada «Cuándo usarlo»Úsalo si tu integración corre en Node.js 22 o en otro runtime de servidor con fetch. Aunque el
paquete no depende de APIs exclusivas de Node, no coloques una API key en JavaScript enviado al
navegador. Las credenciales deben permanecer en tu backend.
Instalar y configurar
Sección titulada «Instalar y configurar»pnpm add ./cierrelisto-sdk-0.1.0.tgzAunque se instale desde un archivo, el nombre de importación es @cierrelisto/sdk.
import { CierreListoClient } from "@cierrelisto/sdk";
function requiredEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Falta la variable ${name}`); return value;}
const cierreListo = new CierreListoClient({ apiKey: requiredEnv("CIERRELISTO_API_KEY"), environment: "sandbox",});
const taxpayerId = requiredEnv("CIERRELISTO_TAXPAYER_ID");Ambientes disponibles:
| Valor | Host |
|---|---|
sandbox |
sandbox.api.cierrelisto.com |
precertification |
precert.api.cierrelisto.com |
certification |
cert.api.cierrelisto.com |
production |
api.cierrelisto.com |
Para pruebas locales puedes proporcionar baseUrl en lugar de environment; el constructor
rechaza que falten ambos o que se combinen. El SDK exige HTTPS salvo loopback. No coloques
credenciales en la URL, query string o fragmentos.
Crear sin duplicar
Sección titulada «Crear sin duplicar»-
Usa un identificador comercial estable
external_iddebe identificar la operación en tu sistema. No lo regeneres durante un reintento. -
Asigna una clave a la intención
idempotencyKeyrepresenta la intención exacta de crear la factura. Si pierdes la respuesta, repite el mismo cuerpo con la misma clave. -
Conserva los metadatos
Guarda
result.body.data.id,external_idyresult.http.requestId. SiidempotencyReplayedestrue, recibiste el resultado guardado de la intención original.
import { CierreListoClient, type CreateInvoiceRequest,} from "@cierrelisto/sdk";
const invoice = { external_id: "erp-venta-2026-000184", ecf_type: "31", issue_date: "2026-07-29", currency: "DOP", payment_type: "credit", buyer: { legal_name: "CLIENTE DE PRUEBA SRL", tax_id: "131246796", }, lines: [ { line_id: "1", description: "Servicios profesionales", item_kind: "service", billing_indicator: 1, unit_code: 28, quantity: "1", unit_price: "70000.00", discount: null, taxes: [ { code: "ITBIS", label: "ITBIS 18%", rate_percent: "18", }, ], }, ], declared_totals: { subtotal: "70000.00", discount_total: "0.00", tax_total: "12600.00", total: "82600.00", },} satisfies CreateInvoiceRequest;
const result = await cierreListo.createInvoice({ taxpayerId, idempotencyKey: invoice.external_id, invoice,});
if (result.ok) { console.log(result.body.data.id, result.http.requestId);} else { console.error(result.kind);}Los importes son strings decimales. El compilador detecta errores de forma y el SDK repite la validación al ejecutar, antes de usar la red.
Leer y reconciliar
Sección titulada «Leer y reconciliar»const page = await cierreListo.listInvoices({ taxpayerId, query: { status: "processing", limit: 50, },});
if (page.ok) { for (const invoice of page.body.data) { console.log(invoice.external_id, invoice.status); } console.log("cursor siguiente", page.body.meta.next_cursor);}const invoice = await cierreListo.getInvoice({ taxpayerId, invoiceId,});const documents = await cierreListo.findFiscalDocuments({ taxpayerId, externalId: "erp-venta-2026-000184",});
if (documents.ok) { const document = documents.body.data[0]; console.log(document?.status, document?.encf);}Descargar XML y representación PDF
Sección titulada «Descargar XML y representación PDF»Las descargas también regresan un resultado discriminado. El SDK valida Content-Type y
X-Request-Id antes de entregar los bytes.
import { writeFile } from "node:fs/promises";
const xml = await cierreListo.downloadFiscalDocumentXml({ taxpayerId, documentId,});
if (xml.ok) { await writeFile("factura.xml", xml.body.bytes);}
const pdf = await cierreListo.downloadFiscalDocumentPdf({ taxpayerId, documentId,});En sandbox estos métodos responden api_error con código artifact_not_ready: el ambiente no
reserva e-NCF, no firma XML y no fabrica artefactos fiscales.
Verificar webhooks con el contrato tipado
Sección titulada «Verificar webhooks con el contrato tipado»El SDK valida la firma HMAC-SHA256 antes de interpretar el JSON. Entrégale los bytes o el string exactos que recibió tu servidor; no reconstruyas el cuerpo desde un objeto.
import { type PublicWebhookEvent, verifyCierreListoWebhook,} from "@cierrelisto/sdk";
function assertNever(value: never): never { throw new Error(`Evento no contemplado: ${JSON.stringify(value)}`);}
function processEvent(event: PublicWebhookEvent): void { switch (event.type) { case "ecf.submitted": console.log(event.data.invoice_id, "presentado"); return; case "ecf.accepted": console.log(event.data.encf, "aceptado"); return; case "ecf.accepted_conditional": console.warn(event.data.messages); return; case "ecf.rejected": console.error(event.data.messages); return; case "ecf.outcome_unknown": console.warn(event.data.invoice_id, "requiere reconciliación"); return; default: assertNever(event); }}
const rawBody = await readRawBody(request);const verification = await verifyCierreListoWebhook({ rawBody, headers: request.headers, endpointSecret: requiredEnv("CIERRELISTO_WEBHOOK_SECRET"),});
if (!verification.ok) { console.error(verification.reason); return new Response("Firma inválida", { status: 400 });}
const { event, deliveryId } = verification;console.log("delivery", deliveryId);processEvent(event);event es una unión discriminada: al comprobar event.type, TypeScript restringe automáticamente
event.data. El helper también valida CierreListo-Delivery, compara CierreListo-Event con el
cuerpo y aplica por defecto una tolerancia de cinco minutos.
Durante una rotación puedes aceptar temporalmente ambos secretos:
await verifyCierreListoWebhook({ rawBody, headers: request.headers, endpointSecret: [currentSecret, previousSecret],});Profundizar en firma, deduplicación y reintentos →
Manejar errores exhaustivamente
Sección titulada «Manejar errores exhaustivamente»Todos los métodos devuelven CierreListoResult<T>. Comprueba ok; TypeScript restringirá el resto
por kind.
import type { CierreListoFailure } from "@cierrelisto/sdk";
function describeFailure(failure: CierreListoFailure): string { switch (failure.kind) { case "api_error": return `${failure.error.code}: ${failure.error.message}`; case "client_validation_error": return `Solicitud inválida: ${JSON.stringify(failure.issues)}`; case "invalid_response": return `Respuesta fuera de contrato: ${failure.requestId ?? "sin request_id"}`; case "transport_error": return `No se pudo conectar: ${failure.message}`; }}kind |
Qué significa | Acción normal |
|---|---|---|
api_error |
El servidor respondió un error público válido | Decide con error.code y el status |
client_validation_error |
La entrada no cumple el contrato | Corrige antes de reintentar |
invalid_response |
La respuesta no coincide con el contrato compartido | Detén el flujo y reporta requestId |
transport_error |
No hubo una respuesta HTTP utilizable | Reintenta con la misma clave idempotente |
En un api_error, failure.error contiene los campos públicos estables y failure.body conserva
el envelope completo, incluidas extensiones compatibles.
Cancelación y request IDs
Sección titulada «Cancelación y request IDs»Cada método acepta signal y un requestId opcional:
const controller = new AbortController();
const result = await cierreListo.getInvoice({ taxpayerId, invoiceId, requestId: "req_0123456789abcdef0123456789abcdef", signal: controller.signal,});Si no envías requestId, el API genera uno. El SDK exige que el header y el cuerpo coincidan; esta
comprobación evita mezclar respuestas durante la observabilidad o a través de un proxy defectuoso.