Ir al contenido

Primera factura en sandbox

El objetivo de la primera prueba no es obtener un número “bonito”. Es demostrar que tu sistema puede crear una operación una sola vez, conservar sus identificadores y llegar a un resultado terminal reproducible.

Necesitas:

  • acceso habilitado al piloto;
  • credencial de sandbox;
  • contribuyente y tipos e-CF asignados;
  • un external_id que tu sistema no reutilizará;
  • un proceso de consulta que conserve los identificadores devueltos;
  • acceso a la referencia contractual.

No necesitas configurar prefijo comercial, numeración, dirección pública, certificado ni secuencias. CierreListo toma el RNC y la razón social del contribuyente asignado a la credencial y usa una dirección sintética exclusivamente para ejecutar el preflight interno. Nada de esa simulación se convierte en una factura fiscal ni se promueve a otro ambiente.

  1. Construye una operación mínima

    Incluye fecha, tipo de comprobante, receptor, moneda, líneas, impuestos, descuentos, pagos y totales declarados según el modelo publicado.

  2. Asigna identidad

    Conserva tu external_id y genera una Idempotency-Key exclusiva para la intención de crear esa factura.

  3. Usa la operación publicada

    Abre “Crear factura” en la referencia API. Copia el ejemplo generado desde OpenAPI y sustituye únicamente los valores del fixture.

  4. Interpreta la respuesta inicial

    El 202 confirma que CierreListo registró la intención. En sandbox, status: accepted identifica un resultado sintético de validación; nunca significa que la DGII aceptó el comprobante.

  5. Persiste referencias

    Guarda external_id, la clave de idempotencia, los identificadores devueltos y request_id antes de esperar el resultado.

  6. Sigue el ciclo

    Consulta la factura o búscala por external_id. Sandbox v1 no genera eventos webhook.

  7. Reconcilia

    Compara tu operación, el recurso de CierreListo y el resultado esperado del fixture.

Ejecuta al menos:

  1. una operación aceptada;
  2. una validación fallida;
  3. la misma solicitud y misma clave;
  4. la misma clave con cuerpo distinto;
  5. la consulta por external_id;
  6. la consulta del documento fiscal sintético y el rechazo esperado de sus artefactos.

El documento sintético de sandbox conserva identificadores y estados para reconciliación, pero sus endpoints XML/PDF responden 409 artifact_not_ready: sandbox no reserva e-NCF, no firma XML y no debe fabricar artefactos con apariencia fiscal.

La integración está lista para avanzar cuando puede reconstruir el historial de una operación sin depender de una pantalla ni de un log completo. En el siguiente ambiente también debe tolerar eventos repetidos y reconciliar el estado actual por API.

Los ejemplos descargables se regeneran desde el mismo openapi.json que construye la referencia. Necesitan una API key y el identificador del contribuyente asignado a esa integración:

Ventana de terminal
export CIERRELISTO_API_KEY="fh_<prefijo>_<secreto>"
export CIERRELISTO_TAXPAYER_ID="uuid-del-contribuyente"

CIERRELISTO_API_URL usa el sandbox por defecto. No reutilices una credencial de otro ambiente.

El tipo 31 representa una factura de crédito fiscal dentro del alcance actual de la API pública.

create-invoice-31.shDescargar
#!/usr/bin/env bash
set -euo pipefail

: "${CIERRELISTO_API_KEY:?Define CIERRELISTO_API_KEY}"
: "${CIERRELISTO_TAXPAYER_ID:?Define CIERRELISTO_TAXPAYER_ID}"

CIERRELISTO_API_URL="${CIERRELISTO_API_URL:-https://sandbox.api.cierrelisto.com}"
CIERRELISTO_EXTERNAL_ID="${CIERRELISTO_EXTERNAL_ID:-sandbox-31-$(date +%s)}"
CIERRELISTO_IDEMPOTENCY_KEY="${CIERRELISTO_IDEMPOTENCY_KEY:-$CIERRELISTO_EXTERNAL_ID}"

curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$CIERRELISTO_API_URL/v1/taxpayers/$CIERRELISTO_TAXPAYER_ID/invoices" \
  --header "Authorization: Bearer $CIERRELISTO_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $CIERRELISTO_IDEMPOTENCY_KEY" \
  --data @- <<JSON
{
  "external_id": "$CIERRELISTO_EXTERNAL_ID",
  "ecf_type": "31",
  "issue_date": "2026-07-29",
  "due_date": "2026-08-28",
  "currency": "DOP",
  "language": "es",
  "payment_type": "credit",
  "buyer": {
    "legal_name": "CLIENTE DE PRUEBA SRL",
    "tax_id": "131246796",
    "email": "cuentas@cliente.example",
    "address": {
      "line1": "Av. Abraham Lincoln 1001",
      "city": "Santo Domingo de Guzmán",
      "province_or_state": "Distrito Nacional",
      "country_code": "DO"
    }
  },
  "lines": [
    {
      "line_id": "1",
      "description": "Servicios profesionales de julio de 2026",
      "sku": "SERV-CONT-001",
      "unit": "servicio",
      "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"
  },
  "payment_terms": "Pago por transferencia dentro de 30 días."
}
JSON

Ejecútalo con:

Ventana de terminal
./create-invoice-31.sh

El tipo 32 usa el mismo contrato estructurado. El servidor aplica las reglas del tipo seleccionado.

create-invoice-32.shDescargar
#!/usr/bin/env bash
set -euo pipefail

: "${CIERRELISTO_API_KEY:?Define CIERRELISTO_API_KEY}"
: "${CIERRELISTO_TAXPAYER_ID:?Define CIERRELISTO_TAXPAYER_ID}"

CIERRELISTO_API_URL="${CIERRELISTO_API_URL:-https://sandbox.api.cierrelisto.com}"
CIERRELISTO_EXTERNAL_ID="${CIERRELISTO_EXTERNAL_ID:-sandbox-32-$(date +%s)}"
CIERRELISTO_IDEMPOTENCY_KEY="${CIERRELISTO_IDEMPOTENCY_KEY:-$CIERRELISTO_EXTERNAL_ID}"

curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$CIERRELISTO_API_URL/v1/taxpayers/$CIERRELISTO_TAXPAYER_ID/invoices" \
  --header "Authorization: Bearer $CIERRELISTO_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $CIERRELISTO_IDEMPOTENCY_KEY" \
  --data @- <<JSON
{
  "external_id": "$CIERRELISTO_EXTERNAL_ID",
  "ecf_type": "32",
  "issue_date": "2026-07-29",
  "due_date": "2026-08-28",
  "currency": "DOP",
  "language": "es",
  "payment_type": "credit",
  "buyer": {
    "legal_name": "CLIENTE DE PRUEBA SRL",
    "tax_id": "131246796",
    "email": "cuentas@cliente.example",
    "address": {
      "line1": "Av. Abraham Lincoln 1001",
      "city": "Santo Domingo de Guzmán",
      "province_or_state": "Distrito Nacional",
      "country_code": "DO"
    }
  },
  "lines": [
    {
      "line_id": "1",
      "description": "Servicios profesionales de julio de 2026",
      "sku": "SERV-CONT-001",
      "unit": "servicio",
      "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"
  },
  "payment_terms": "Pago por transferencia dentro de 30 días."
}
JSON

El script usa el cliente oficial tipado y funciona en Node.js 22 o posterior. El SDK valida la solicitud antes de enviarla, valida la respuesta contra los mismos schemas Zod que generan OpenAPI y distingue errores del API, validación local, transporte y respuestas fuera de contrato.

Durante el piloto, instala el artefacto .tgz versionado que recibiste en tu onboarding:

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

El nombre de importación continúa siendo @cierrelisto/sdk. La instalación directa desde el registro se documentará cuando esa versión esté publicada.

Usa 31 por defecto; cambia el tipo con CIERRELISTO_ECF_TYPE=32.

create-invoice.tsDescargar
/* biome-ignore-all lint/suspicious/noConsole: This executable CLI example reports its result to stdout. */

import {
  CierreListoClient,
  type CierreListoFailure,
  type CreateInvoiceRequest,
  type PublicEcfTypeCode,
} 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 apiUrl = process.env.CIERRELISTO_API_URL ?? "https://sandbox.api.cierrelisto.com";
const apiKey = requiredEnv("CIERRELISTO_API_KEY");
const taxpayerId = requiredEnv("CIERRELISTO_TAXPAYER_ID");
const externalId = process.env.CIERRELISTO_EXTERNAL_ID ?? `sandbox-${crypto.randomUUID()}`;
const idempotencyKey = process.env.CIERRELISTO_IDEMPOTENCY_KEY ?? externalId;
const configuredType = process.env.CIERRELISTO_ECF_TYPE ?? "31";

if (configuredType !== "31" && configuredType !== "32") {
  throw new Error("CIERRELISTO_ECF_TYPE debe ser 31 o 32");
}

function describeFailure(failure: CierreListoFailure): string {
  switch (failure.kind) {
    case "api_error":
      return `${failure.error.code}: ${failure.error.message} (${failure.http.requestId})`;
    case "client_validation_error":
      return `${failure.message}\n${JSON.stringify(failure.issues, null, 2)}`;
    case "invalid_response":
      return `${failure.message} (HTTP ${failure.status}, ${failure.requestId ?? "sin request_id"})`;
    case "transport_error":
      return `No fue posible conectar con CierreListo: ${failure.message}`;
  }
}

const ecfType: PublicEcfTypeCode = configuredType;
const payload = {
  external_id: externalId,
  ecf_type: ecfType,
  issue_date: "2026-07-29",
  due_date: "2026-08-28",
  currency: "DOP",
  language: "es",
  payment_type: "credit",
  buyer: {
    legal_name: "CLIENTE DE PRUEBA SRL",
    tax_id: "131246796",
    email: "cuentas@cliente.example",
    address: {
      line1: "Av. Abraham Lincoln 1001",
      city: "Santo Domingo de Guzmán",
      province_or_state: "Distrito Nacional",
      country_code: "DO",
    },
  },
  lines: [
    {
      line_id: "1",
      description: "Servicios profesionales de julio de 2026",
      sku: "SERV-CONT-001",
      unit: "servicio",
      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",
  },
  payment_terms: "Pago por transferencia dentro de 30 días.",
} satisfies CreateInvoiceRequest;
const client =
  apiUrl === "https://sandbox.api.cierrelisto.com"
    ? new CierreListoClient({ apiKey, environment: "sandbox" })
    : new CierreListoClient({ apiKey, baseUrl: apiUrl });

const result = await client.createInvoice({
  taxpayerId,
  idempotencyKey,
  invoice: payload,
});

if (!result.ok) {
  console.error(describeFailure(result));
  process.exitCode = 1;
} else {
  console.log(JSON.stringify(result.body, null, 2));
  console.log(`request_id: ${result.http.requestId}`);
  if (result.http.idempotencyReplayed) {
    console.log("La respuesta fue recuperada de la operación idempotente original.");
  }
}
Ventana de terminal
CIERRELISTO_ECF_TYPE=32 pnpm dlx tsx create-invoice.ts

Diseñar la integración con el SDK TypeScript →

Entender el modelo de factura →