Ir al contenido

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.

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

Ventana de terminal
pnpm add ./cierrelisto-sdk-0.1.0.tgz

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

  1. Usa un identificador comercial estable

    external_id debe identificar la operación en tu sistema. No lo regeneres durante un reintento.

  2. Asigna una clave a la intención

    idempotencyKey representa la intención exacta de crear la factura. Si pierdes la respuesta, repite el mismo cuerpo con la misma clave.

  3. Conserva los metadatos

    Guarda result.body.data.id, external_id y result.http.requestId. Si idempotencyReplayed es true, 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.

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);
}

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.

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 →

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.

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.

Continuar con idempotencia y external_id →