factadesarrolladores API 0.1.0-esqueleto

@facta-dte/api

English version

El único cliente oficial. Node 22 y 24, y también Deno y Bun: fetch y crypto.randomUUID son los dos únicos globales que toca, y los dos son estándar en todas partes.

Instalación

Instale el cliente oficial desde npm · @facta-dte/api. El código, los ejemplos, las versiones y las incidencias están en GitHub · Facta-DTE/facta-api-sdk. Esta referencia describe la versión 0.3.0; compruebe la versión publicada en npm antes de usar sus funciones nuevas. El tag latest selecciona la versión estable aprobada.

sh
pnpm add @facta-dte/api

El portal valida esta página contra el código fuente del paquete publicado (npm incluye src/client.ts y src/errors.ts), no contra una copia: la versión de arriba es la que se comprobó. El paquete tiene varias entradas:

EntradaPara qué sirve
@facta-dte/apiEl cliente Facta, los tipos y los adaptadores portables. Node 22+, Deno y Bun.
@facta-dte/api/node, @facta-dte/api/file-archiveSolo Node 22+ y Deno 2: archivo cifrado en disco (FileInvoiceArchive) y lectura de configuración.
@facta-dte/api/serverPara su servidor: createFactaHandler y createFactaSession montan la ventana de emisión sin que el navegador reciba ninguna credencial de Facta.
@facta-dte/api/browser, @facta-dte/api/react (y react/styles.css)La ventana de emisión y los componentes de documentos para el navegador. Hablan solo con el servidor de usted.

¿Programa con un agente de código? Hay una skill para agentes de IA que le enseña a usar este cliente sin adivinar.

Las guías de la ventana de React y de su servidor están en el repositorio del SDK: guides/react.md y guides/react-server.md.

Configuración

typescript
import { Facta } from "@facta-dte/api";

const facta = new Facta({
  apiKey: process.env.FACTA_API_KEY!,
  // Solo hace falta para emitir, sign y anular. Consultar no la pide.
  signKey: process.env.FACTA_SIGN_KEY!,
});

FactaOptions:

OpciónTipoQué hace
apiKeytexto, requeridafacta_test_… o facta_live_…. Léala del gestor de secretos, no de un archivo del repositorio.
signKeyfactask_…, condicionalHace falta para issue, sign y anular; no para getDocumentStatus. Guárdela fuera del mismo .env que apiKey. Si le pasa una factauk_ por error, el constructor la rechaza por el prefijo.
unlockKeyfactauk_…, condicionalHace falta para abrir localmente los vaults de destinos y catálogo. No viaja en ninguna petición. No la necesita si manda receptor y líneas completas inline, sin customerId ni productId.
baseUrltexto, opcionalPor defecto la URL pública. Una llave facta_test_ sigue emitiendo en pruebas con esa misma URL.
timeoutMsnúmero, opcional, defecto 60000Plazo de la petición entera. El Ministerio puede tardar unos cuarenta segundos en contestar.
maxRetriesnúmero, opcional, defecto 3Cuántas veces reintentar las condiciones donde reintentar es seguro.
fetchfunción, opcionalPara inyectarlo en pruebas.
configFactaConfigV1, opcionalComportamiento versionado y sin secretos (baseUrl, timeoutMs, maxRetries, ticketPaperWidthMm, expectedEnvironment, requiredScopes, allowStaleCatalogReads). Las opciones sueltas de arriba mandan sobre él.
runtimeFactaRuntimeConfigV1, opcionalObjetos ejecutables por defecto: archive, invalidationArchive, remoteDestinations, replicate y printTransport. Cada llamada puede pasar los suyos.
clocktrue, URL o false, opcionalDesde la 0.4.0. Reloj de referencia para las marcas de tiempo del archivo y la firma de subidas a S3. Por defecto true: https://clock.factadte.com/. false usa la hora del equipo. Nunca falla una operación.
clockFetchfunción, opcionalDesde la 0.4.0. fetch que se usa solo para calibrar el reloj.

CallOptions — el segundo argumento de los métodos que escriben:

OpciónQué hace
idempotencyKeyLa suya. Si su sistema ya tiene un identificador para esta venta, páselo: así la operación es idempotente entre reinicios del proceso, no solo entre reintentos de una llamada. Sin ella, el cliente genera una y la reusa en todos los reintentos.
signalUn AbortSignal para cancelar. Los reintentos se detienen con la misma señal.

Modelos y cobertura actual

Los tipos públicos viven en src/types.ts del repositorio del SDK y se exportan desde mod.ts. Para el SDK, los modelos principales son:

ModeloRepresenta
DteRequestLa petición para issue, prepare o sign.
Recipient y AddressDatos fiscales inline; customerId es una referencia opcional al catálogo cifrado de esa llave.
ExportRecipient, ExcludedSubjectRecipientDatos del receptor específicos para DTE 11 y 14.
RelatedDocument, ExportDetailsDocumentos corregidos en notas 05/06 y datos de exportación 11.
LineItemUna línea de la venta. cantidad siempre se envía; productId permite resolver descripción, precio, tipo de artículo y unidad del catálogo cifrado.
IssueResultUnión discriminada por estado: SealedDte o DteInContingency.
PreparedDte, DocumentStatus, InvalidationResult, DtePage, HoldingPageResultados de prepare, getDocumentStatus, anular, listar y recuperar documentos.
Status, RateLimitWindow, Totals, FactaErrorEstado de llave, límites, importes calculados por el servidor y errores tipados.

DteType y DteRequest cubren los seis tipos que admite el API (01, 03, 05, 06, 11, 14). La solicitud es una unión discriminada: al elegir tipoDte: "11", TypeScript exige el receptor extranjero y el bloque exportacion; para 05 y 06 exige al menos un documento relacionado. Los tipos ayudan a construir el payload, pero el API sigue siendo la autoridad de validación fiscal. Consulte el contrato OpenAPI para restricciones completas.

El catálogo no es requisito para emitir. Si no guarda clientes con Facta, mande los datos inline en receptor; el SDK los pasa a la API y no crea ni actualiza una ficha. Una FE (01) puede omitir receptor si es consumidor final. Un CCF (03) requiere nombre, NIT, NRC, actividad, dirección y correo, aunque el cliente no exista en el catálogo.

El producto debe tener un item_type válido y la base de su precio debe coincidir con el documento: IVA incluido para FE (01), excluido para CCF y los demás tipos gravados. Si no coincide, resuelva la línea con un precioUni inline en la base que espera ese DTE, o corrija vat_included en el catálogo. El SDK no convierte IVA; el cálculo fiscal permanece en el servidor. En FSE (14) el precio se conserva sin conversión.

Ejemplo de nota de crédito sobre un documento electrónico de la misma empresa:

typescriptNota de crédito (05)
const nota = await facta.issue({
  tipoDte: "05",
  documentosRelacionados: [{ codigoGeneracion: facturaOriginal.codigoGeneracion }],
  items: [{ descripcion: "Devolución parcial", cantidad: 1, precioUni: 25 }],
});

Una nota de débito (06) usa la misma forma para relacionar el documento; es el único tipo que admite numPagoElectronico:

typescriptNota de débito (06)
const debito = await facta.issue({
  tipoDte: "06",
  documentosRelacionados: [{ codigoGeneracion: facturaOriginal.codigoGeneracion }],
  numPagoElectronico: "TRF-99120",
  items: [{ descripcion: "Cargo adicional acordado", cantidad: 1, precioUni: 5 }],
});

Para un documento que no esté en el índice de la empresa, envíe los cuatro campos del documento relacionado (tipoDocumento, numeroDocumento, fechaEmision y, si es papel, tipoGeneracion: 1). En notas con más de un documento, asigne numeroDocumento también a cada línea.

Ejemplo mínimo de exportación y de factura a sujeto excluido. Los datos aquí son ilustrativos; Hacienda determina la aceptación fiscal final.

typescriptExportación (11)
const exportacion = await facta.issue({
  tipoDte: "11",
  receptor: {
    nombre: "Example Imports LLC",
    numDocumento: "EIN-123456",
    codPais: "US",
    nombrePais: "Estados Unidos",
    complemento: "100 Market Street, Miami, FL",
    tipoPersona: 2,
    descActividad: "Importación de productos",
    correo: "billing@example.com",
  },
  exportacion: { tipoItemExpor: 1 },
  items: [{ descripcion: "Producto de muestra", cantidad: 2, precioUni: 40 }],
});
typescriptSujeto excluido (14)
const compra = await facta.issue({
  tipoDte: "14",
  receptor: {
    nombre: "Proveedor independiente",
    numDocumento: "061234567",
    direccion: {
      departamento: "06",
      municipio: "20",
      complemento: "San Miguel",
    },
  },
  aplicarReteRenta: false,
  items: [{ descripcion: "Servicio profesional", cantidad: 1, precioUni: 100 }],
});
typescriptEmisión sin customerId ni cliente guardado
const dte = await facta.issue({
  tipoDte: "03",
  receptor: {
    nombre: "Comercial Ejemplo, S.A. de C.V.",
    tipoDocumento: "36",
    numDocumento: "06141234567890",
    nrc: "1234567",
    codActividad: "46510",
    descActividad: "Venta al por mayor de equipo",
    direccion: {
      departamento: "06",
      municipio: "20",
      complemento: "Colonia Centro, San Miguel",
    },
    correo: "compras@example.com",
  },
  items: [{ descripcion: "Equipo", cantidad: 1, precioUni: 125 }],
}, { idempotencyKey: "pedido-ERP-1042" });

Este ejemplo no usa unlockKey porque no consulta referencias del catálogo. Sí necesita la signKey para sign. Para una FE anónima, omita receptor; para una FE identificada, envíe en receptor los campos que quiera incluir.

Métodos del cliente

status()

No manda la llave de firma. Devuelve Status.

Lo primero que conviene llamar. Confirma que la llave sirve, dice para qué empresa emite, en qué ambiente, si puede firmar y cuánto cupo le queda. Cuando existe registro público del certificado asociado, también incluye su huella, NIT y vigencia; nunca entrega la llave privada ni el contenido del vault. No exige ningún alcance.

typescript
const estado = await facta.status();

console.log(estado.ambiente);              // "00" en pruebas, "01" en producción
console.log(estado.llave.alcances);        // ["issue", "query", "download"]
console.log(estado.limites.hora?.remaining);

diagnose(options?)

Revisa la disponibilidad de la API, el emisor, el alcance para el tipo de DTE, las revisiones de firma/destinos/catálogo y los datos públicos del certificado cuando están registrados. Puede recibir { dteType, archive } para comprobar el tipo de DTE y el archivo local. No abre vaults ni reserva un correlativo. Devuelve canIssue, canQuery, canDownload, canIssueAndArchive, las comprobaciones y el conteo de operaciones pendientes del archivo; ese conteo es null si no se pasó archivo o no se pudo leer su journal. Si falla la consulta de estado, devuelve un informe bloqueado con el código seguro del error.

typescript
const informe = await facta.diagnose({ dteType: "03", archive });
if (!informe.canIssueAndArchive) {
  throw new Error(informe.checks.map((item) => item.message).join("; "));
}

Clientes y productos sincronizados

Con una llave en modo cifrado, el SDK descifra el snapshot en el proceso del integrador y los métodos de búsqueda filtran localmente los registros autorizados a esa llave. Cada llamada compara la revisión publicada con la revisión local y vuelve a sincronizar si cambió, y requieren unlockKey. Leer el snapshot no crea ni cambia registros; para eso están los métodos de escritura de abajo.

typescript
const cliente = await facta.getCustomer("cliente-id");
const clientes = await facta.searchCustomers("Comercial Ejemplo", { limit: 20 });
const productos = await facta.searchProducts("cafe");
const todoElCatalogoActivo = await facta.listProducts();
MétodoResultado y comportamiento
listCustomers(options?)Devuelve los clientes del snapshot autorizado; allowStale es opt-in.
getCustomer(id, options?)Busca por ID y devuelve null si no existe en el snapshot.
searchCustomers(query, options?)Busca nombre, documento, NRC y correo; límite 1–500, 50 por defecto.
listProducts(options?)Devuelve productos activos; includeInactive los incluye todos.
getProduct(id, options?)Devuelve null si no existe o está inactivo.
searchProducts(query, options?)Busca descripción, código y código de barras de productos activos; límite 1–500, 50 por defecto.

Las búsquedas ignoran mayúsculas y acentos, y solo consultan los campos descargados en el snapshot local.

Crear, editar, desactivar y reactivar

Si la empresa guarda su catálogo en texto plano y el dueño habilitó «Permitir administrar clientes y productos desde el API», una llave con el alcance catalog:write escribe en el catálogo: createCustomer, updateCustomer y deactivateCustomer, y lo mismo para productos. No existe borrado definitivo: desactivar envía DELETE y deja el registro con activo: false, fuera de las listas. Se reactiva por la API, con { activo: true }. La aplicación web no lista los clientes desactivados, así que no es ahí donde se reactivan.

typescript
const inactivos = await facta.listCustomers({ includeInactive: true });
await facta.deactivateCustomer(cliente.id);              // DELETE → activo: false
await facta.updateCustomer(cliente.id, { activo: true }); // PATCH  → activo: true

Crear un cliente con el documento de uno desactivado responde 409 catalog_duplicate con details.id y details.activo: false: reactívelo en lugar de crear otro. Por HTTP directo son DELETE /v1/customers/{id} y PATCH /v1/customers/{id} con {"activo": true}; la lista acepta ?incluirInactivos=true.

Catálogo legible por la API

El dueño puede activar «Catálogo legible por la API» para una llave. Con esa llave, receptor.customerId y items[].productId los resuelve el servidor de Facta a partir del catálogo que la aplicación publicó: no hace falta unlockKey, no hace falta sincronizar nada en su proceso y un cambio de catálogo nunca deja al SDK con una copia vieja. status() lo anuncia en llave.catalogMode ("readable"; "encrypted" o ausente en servidores anteriores) y una empresa que guarda su catálogo sin cifrar lo anuncia también como "readable". El SDK consulta ese modo antes de emitir una solicitud con referencias y lo recuerda un minuto; si la consulta falla, cae al comportamiento de siempre, resolver localmente con unlockKey, así que esa comprobación nunca es la causa de que una emisión falle.

Los datos que usted escriba en la solicitud ganan sobre los del catálogo, y un identificador desconocido se rechaza antes de reservar un correlativo. Lo que puede ver:

RespuestaQué pasó
not_found, details.reason: "catalog_unknown_id"No existe un cliente o producto con ese id para la llave. details.catalogStale: true dice que el catálogo publicado va por detrás: abra Facta para que se sincronice y reintente.
invalid_request, details.reason: "catalog_not_published"El catálogo legible todavía no se publicó: abra Facta con el vault abierto, o mande receptor y líneas completos.
invalid_request, details.reason: "catalog_record_encrypted"El registro existe pero la empresa aún está convirtiendo su catálogo: termine la conversión en Facta o mande los datos completos.

Con una llave en modo encrypted, syncCatalog() falla con no_storage_destination (422) y details.reason: "catalog_out_of_sync" cuando el catálogo de la llave está desactualizado; el detalle trae syncStatus, desiredRevision y publishedRevision. El mensaje pide abrir Facta para sincronizar. status() también devuelve catalogoLegible (publicado, revisionPublicada, revisionActual) para una llave legible.

syncDestinations(), syncCatalog() y catalogState()

El dueño sincroniza los destinos y el catálogo desde la aplicación web. El SDK usa unlockKey para abrir las envolturas localmente en el proceso integrador; la contraseña no se envía a Facta. syncDestinations() devuelve la configuración descifrada en memoria y no sube facturas a esos destinos. syncCatalog() reemplaza la copia local por el snapshot publicado más reciente. Ambos fallan si falta unlockKey o la revisión que necesitan aún no se publicó desde la aplicación. Un error criptográfico con una clave incorrecta puede venir de Web Crypto; no es una respuesta de la API.

catalogState() compara la revisión local con el estado público y devuelve missing, fresh o stale, las revisiones disponibles y un código de error seguro; nunca devuelve datos de clientes o productos. Las llamadas de lectura se actualizan al detectar una revisión nueva. allowStale: true sirve solo para búsquedas y lecturas informativas cuando no se puede consultar el estado; el SDK nunca resuelve referencias fiscales con un snapshot viejo.

typescript
const { destinos } = await facta.syncDestinations();
const { customers, products } = await facta.syncCatalog();
const estadoCatalogo = await facta.catalogState();

El paquete tiene adaptadores BYOS para S3/S3-compatible, Supabase Storage, OneDrive, Google Drive y el bridge local FTP/SFTP. El integrador los ejecuta en su proceso; las credenciales descifradas permanecen en memoria. FTP/SFTP requiere el bridge emparejado. El almacenamiento Facta usa un contrato separado de llave API: el servidor escribe las copias JSON/PDF y el SDK consulta sus recibos. No reenvíe la sesión del usuario ni credenciales del bucket al SDK. Confirme con getStorageStatus() la capacidad del servidor antes de depender del almacenamiento Facta.

getStorageStatus(options?)

Devuelve ManagedStorageStatus; exige download y no firma ni emite. capabilityVersion: 1 identifica el contrato. managed.ready confirma integración, cobertura, bucket y cupo para nuevas escrituras ahora; una reserva concurrente vuelve a comprobar el cupo. byos.ready representa un destino BYOS verificado. accessUntil describe la ventana de lectura, no un respaldo probado.

getDocumentCopies(options?)

Devuelve ManagedDocumentCopy[]; exige download. Use generationCode para consultar los recibos JSON/PDF de un DTE de la empresa y ambiente de la llave. Incluye estados stored, pending y failed, hash, bytes y fecha duradera; no devuelve documentos, rutas o credenciales. Puede cancelar con signal.

retryDocumentStorage(generationCode, options?)

Devuelve ManagedStorageReceipt; exige download y repara solamente las copias de un DTE ya sellado. No firma, no vuelve a emitir ni gasta correlativos. Si faltan los bytes originales, el resultado lo indica; no reconstruye una factura desde el catálogo. stored prueba una copia duradera, no retención temporal.

typescriptInspeccionar y reparar copias existentes
const capability = await facta.getStorageStatus();
const copies = await facta.getDocumentCopies({ generationCode });
if (copies.some(copy => copy.state === "pending" || copy.state === "failed")) {
  const receipt = await facta.retryDocumentStorage(generationCode);
  // Check each artifact state; a repair result does not issue another DTE.
}

diagnose() consulta esta capacidad. Facta listo satisface el destino duradero sin exigir una bóveda BYOS; la firma y las referencias del catálogo conservan sus comprobaciones. Una capacidad presente pero inválida bloquea la emisión. Las rutas anteriores ausentes quedan como desconocidas, sin inventar copias. storage_unsupported identifica rutas anteriores no disponibles; storage_contract_invalid identifica contratos inválidos; storage_unavailable indica que el servicio de almacenamiento no está disponible.

Un recibo adjunto inválido no elimina el éxito fiscal: se omite storage y se expone storageErrorCode: "storage_contract_invalid". El archivo cifrado conserva esa condición pendiente hasta obtener un recibo válido. La recuperación compara el hash y los bytes del recibo con los archivos locales exactos. No emita con una nueva clave para corregir una copia pendiente.

Los ejemplos de flujos del archivo fuente examples/workflows.ts se invocan explícitamente: issueWithCopies combina el recibo Facta con archivo local y BYOS opcional; recoverAfterRestart recupera la misma operación. Requieren una versión fuente que incluya estos métodos; no afirman que estén publicados en la versión actual de npm. El ticket sigue fuera del almacenamiento Facta (unsupportedKinds vale ["ticket"]). El evento de anulación sí se guarda allí, como archivo de tipo invalidation (supportedKinds lo incluye), y además se archiva en el journal de invalidateAndArchive().

issue(request, options?)

Manda la llave de firma. Devuelve SealedDte | DteInContingency.

Prepara, firma y transmite en una sola operación. Es el camino normal. Devuelve una unión: estado === "sellado" trae selloRecibido y totales; estado === "contingencia" no, porque todavía no hay veredicto — y el tipo lo obliga. Errores propios: mh_rejected, validation_failed, no_storage_destination, amount_limit.

typescript
import { Facta, FactaError } from "@facta-dte/api";

const facta = new Facta({
  apiKey: process.env.FACTA_API_KEY!,
  signKey: process.env.FACTA_SIGN_KEY!,
  unlockKey: process.env.FACTA_UNLOCK_KEY!,
});

try {
  const dte = await facta.issue(
    {
      tipoDte: "03",
      receptor: { customerId: "374114b6-e957-4c7a-8911-dd6381b1e0ea" },
      items: [{ descripcion: "Integración de la API", cantidad: 1, precioUni: 25 }],
    },
    { idempotencyKey: "venta-2026-09-02-00417" },
  );

  console.log(dte.estado, dte.numeroControl);
  if (dte.estado === "sellado") console.log(dte.selloRecibido, dte.totales.totalPagar);
} catch (error) {
  if (error instanceof FactaError && error.isRejection) {
    console.error("rechazado:", error.message, "· gastó:", error.spent?.numeroControl);
  } else throw error;
}

issueAndArchive(request, options)

Emite y conserva una copia cifrada recuperable. Requiere un InvoiceArchive, un operationId estable y un idempotencyKey estable. El SDK comprueba que el archivo pueda escribir antes de pedir un correlativo y conserva los bytes exactos del JSON, PDF, JWS y ticket. La descarga o el guardado posterior pueden fallar aunque Hacienda ya haya aceptado la factura: revise result.archive.state y result.archive.detail por separado del resultado fiscal.

El paquete incluye FileInvoiceArchive en @facta-dte/api/file-archive para Node 22+ y Deno 2. Configure su frase de cifrado como un secreto independiente de FACTA_UNLOCK_KEY; deje un solo proceso escribiendo en cada directorio local. En otros runtimes puede implementar InvoiceArchive con almacenamiento transaccional.

typescript
import { FileInvoiceArchive } from "@facta-dte/api/file-archive";

const archive = await FileInvoiceArchive.open({
  directory: process.env.FACTA_ARCHIVE_DIRECTORY!,
  passphrase: process.env.FACTA_ARCHIVE_PASSPHRASE!,
});
const archivada = await facta.issueAndArchive(solicitud, {
  archive,
  operationId: order.id,
  idempotencyKey: order.id,
  ticketPaperWidthMm: 80,
});

remoteDestinations puede replicar cada artefacto durante el flujo. Las copias remotas tienen su propio estado por destino y tipo de archivo; una falla de réplica nunca vuelve a emitir el DTE.

Copias en sus destinos (BYOS), por defecto

Si usted configuró unlockKey y no pasó remoteDestinations, issueAndArchive y recoverOperation abren el snapshot de destinos de la llave y replican el JSON y el PDF archivados a cada destino que se pueda escribir sin una persona delante: compatibles con S3 (S3, R2, B2, Wasabi, Spaces, MinIO) y Supabase Storage. Las rutas son las que lee la aplicación, DTE/pruebas/AAAA/MM/<numeroControl>.json|pdf en pruebas y DTE/AAAA/MM/… en producción, tomadas del JSON legal archivado. Los demás tipos de destino (OneDrive, FTP/SFTP) se omiten con una advertencia, porque necesitan la sesión de la aplicación o el bridge. Google Drive no es un destino de la API: nunca viaja en el vault de destinos de una llave y la ruta drive-token responde 403 drive_not_available_for_api. replicate: false, por llamada o en runtime, desactiva esta réplica.

Nada de esto lanza una excepción: el documento ya está sellado. Los problemas llegan como result.warnings, y la copia que no se pudo confirmar queda pendiente para recoverOperation:

warnings[].codeSignifica
byos_not_replicatedEl snapshot de destinos no se pudo abrir, un destino se omitió o una copia no quedó confirmada. destinationId nombra el destino; detail dice por qué, sin credenciales.
copy_report_failedLa copia quedó guardada en su destino pero Facta no recibió el aviso.

Cuando el JSON quedó guardado en un destino, el SDK avisa a Facta con POST /v1/storage/copies/{codigoGeneracion}/byos (alcance issue), solo si el servidor lo anuncia en GET /v1/storage/status (capabilities.byosCopyReport en 1 o más); si no, no manda nada. El resultado queda en remoteCopies[].report ("reported" o "failed"); failed mantiene la operación pendiente y recoverOperation repite el aviso. Un aviso nunca marca una copia como verificada: Facta no puede leer su bucket con sus credenciales, así que lo registra como copia reportada por el SDK (verified: false, writtenBy: "sdk") y comprueba contra la base que el destino sea de la llave y que las rutas sean las canónicas.

La anulación sigue la misma regla desde la 0.6.0, y se explica en invalidate(): el evento se escribe en esos mismos destinos y se avisa con kind: "invalidation", que el servidor anuncia con capabilities.byosCopyReport en 2 o más (o invalidation en supportedKinds).

recoverOperation(operationId, options?)

Recupera una operación del journal después de un reinicio. Pase el ID guardado y, si aplica, request, archive, signal o remoteDestinations en las opciones. La solicitud es obligatoria solo para journals antiguos que no guardaron una copia cifrada. Reutiliza la misma clave de idempotencia; si ya conoce el código de generación, consulta el DTE y descarga los bytes que falten. Si la emisión quedó sin confirmar y el plazo conservador venció, se detiene para reconciliar en vez de enviar una emisión nueva. También puede volver a intentar copias remotas pendientes.

Antes de recuperar, compara la identidad guardada (endpoint, ID público de la llave, NIT emisor y ambiente) con /v1/status. Si cambia alguno, o el journal antiguo no conserva esa identidad, la recuperación automática se detiene. Los artefactos guardados se verifican y reutilizan; solo se descargan los que faltan y nunca se reemplazan con versiones regeneradas.

replicateArchive(operationId, archive, destinations, options?)

Envía los bytes ya archivados a los adaptadores remotos que proporciona el runtime. Verifica sus hashes antes de la primera escritura y registra cada resultado en el journal cifrado. Para un estado unknown, conserve el mismo ID de operación y reconcilie con el check() del adaptador o vuelva a correr la replicación idempotente. Este método nunca emite un DTE.

diagnoseDestinations(operationId, archive, destinations, options?)

Comprueba en modo de solo lectura si cada artefacto archivado existe en los destinos remotos y coincide con su hash. stored, missing, unknown y unsupported distinguen los resultados; una ausencia no dispara una escritura y el diagnóstico no demuestra permiso de escritura.

listPendingOperations(archive?)

Lista resúmenes permitidos de journals locales que aún requieren completar el archivo o reconciliar copias remotas. No devuelve la solicitud fiscal guardada, los campos del representante ni los artefactos; requiere un archivo local listo.

prepare(request, options?)

No manda la llave de firma. Devuelve PreparedDte.

Reserva el correlativo y devuelve el documento canónico con sus totales, sin firma. Para quien quiere ver el documento antes de firmarlo. Reserva el número, así que un prepare sin su sign deja un correlativo entregado, y eso es un hueco que alguien tiene que explicar. Por eso el prepareToken vence a los 15 minutos.

typescript
const preparado = await facta.prepare({
  tipoDte: "01",
  items: [{ descripcion: "Café", cantidad: 2, precioUni: 1.5 }],
});

console.log(preparado.totales.totalPagar);   // ya calculado por el servidor
console.log(preparado.numeroControl);        // el número ya está reservado

sign(prepared, options?)

Manda la llave de firma. Devuelve SealedDte | DteInContingency.

Firma y transmite exactamente lo que devolvió prepare. Pase el documento sin tocar un centavo: el servidor comprueba un MAC sobre su hash canónico, y un cambio se rechaza con prepare_token_invalid en vez de firmarse.

typescript
const dte = await facta.sign(preparado);

Entrega por correo y WhatsApp: deliverEmail(), deliverWhatsApp(), getDelivery() y waitForDelivery()

Emitir nunca espera a la entrega: son peticiones aparte. En issue(request, { deliver }) usted marca los canales y, si el documento queda sellado, la respuesta trae entrega con un token que dura cinco minutos y los estados iniciales (pendiente, con el destinatario enmascarado). Con ese token se pide cada canal por separado. El token es un secreto al portador: guárdelo en su servidor.

typescript
const dte = await facta.issue(venta, {
  idempotencyKey: "venta-1042",
  deliver: {
    email: true, // usa receptor.correo; un texto lo reemplaza solo para la entrega
    whatsapp: { number: "+50370000000", consent: true },
  },
});

if (dte.estado === "sellado" && dte.entrega?.token) {
  await facta.deliverEmail(dte.codigoGeneracion, dte.entrega.token);
  await facta.deliverWhatsApp(dte.codigoGeneracion, dte.entrega.token);
  const entrega = await facta.waitForDelivery(dte.codigoGeneracion);
  console.log(entrega.settled, entrega.canales);
}
MétodoQué hace
deliverEmail(generationCode, token, options?)Inicia el correo. Contesta 200 con el estado final del canal, o 202 con en_proceso. Una segunda llamada devuelve el estado actual sin enviar un segundo mensaje.
deliverWhatsApp(generationCode, token, options?)Igual, por WhatsApp. Se descuenta del saldo prepagado de WhatsApp de la empresa; sin saldo el estado es sin_credito. El contenido es la plantilla aprobada y el documento, nada más.
getDelivery(generationCode, options?)Lee el estado de cada canal. Sirve también después de que el token venció.
waitForDelivery(generationCode, options?)Repite getDelivery hasta que todos los canales esperados sean finales (todo lo que no sea pendiente ni en_proceso; esperando_sello no se espera). Al vencer timeoutMs (60 s por defecto) devuelve lo último que leyó con settled: false en vez de lanzar.

Un canal que no se puede entregar es un estado con su motivo (fallido, sin_credito, sin_consentimiento, no_permitido, vencido), no una excepción, y nunca cambia el resultado fiscal. Un documento en contingencia no trae token: sus canales quedan en esperando_sello. Una llamada con el token vencido lanza FactaError entrega_vencida (410); un token equivocado, entrega_token_invalido. El SDK borra el token de los mensajes y detalles de error.

Alcance y consentimiento. La entrega forma parte de emitir: cualquier llave con el alcance issue puede marcar y entregar los dos canales, sin volver a acuñarla. Una llave acuñada solo con entrega:correo o entrega:whatsapp conserva ese canal. Si la llave no tiene ni issue ni el alcance del canal, la emisión sigue adelante y el canal queda no_permitido. En el cable (entrega.whatsapp.consentimiento) el consentimiento vale true si se omite: pedir WhatsApp es la declaración de que el receptor aceptó recibirlo, y solo un false explícito deja el canal en sin_consentimiento. El ayudante deliver.whatsapp del SDK es más estricto a propósito y sigue exigiendo escribir consent: true. La declaración queda guardada con el id de la llave y el número enmascarado.

getDocumentStatus(generationCode)

No manda la llave de firma. Devuelve DocumentStatus.

Un documento por su código de generación, en el estado en que esté. Contesta también por los rechazados, que no tienen fila de índice pero sí reserva: devolver 404 para un número que Hacienda negó mandaría a buscar un error que no existe.

typescript
const uno = await facta.getDocumentStatus("7875BC7A-9580-441D-94E4-FA455E9D8BD0");
console.log(uno.estado);   // "rechazado", "sellado", "contingencia"…

listDocuments(filters?)

No manda la llave de firma. Devuelve DtePage.

El libro de lo sellado, del más nuevo al más viejo, con filtros por fecha, estado y tipo. La paginación es por cursor, no por página: ?pagina=2 sobre una tabla que crece repite un documento y se salta otro. Un documento rechazado no está aquí. Si al reconciliar aparece un hueco en su numeración, pregunte por ese código con getDocumentStatus. Cada fila puede incluir el nombre y número de documento del receptor. En modo privado, la API abre esos campos para la llave autorizada: el cifrado en reposo no los oculta a la integración. Trata la respuesta como dato personal y evita copiarla a logs generales.

typescript
let cursor: string | null = null;

do {
  const pagina = await facta.listDocuments({
    desde: "2026-09-01",
    hasta: "2026-09-30",
    estado: "sellado",
    limit: 100,
    ...(cursor ? { cursor } : {}),
  });

  for (const fila of pagina.documentos) {
    console.log(fila.numeroControl, fila.totales?.totalPagar);
  }
  cursor = pagina.siguiente;
} while (cursor !== null);

invalidate(generationCode, request, options?)

Manda la llave de firma. Devuelve InvalidationResult.

Anula un documento que Hacienda ya selló. No es un borrado: es un evento, con su propio documento firmado, su propio código y su propio sello. No hay forma de deshacerla. Los tres tipos no son intercambiables: el 1 (error en el documento) y el 3 (otro) exigen motivo y codigoGeneracionReemplazo —emita primero el documento correcto—; el 2 (rescisión) prohíbe nombrar reemplazo. Pedir la anulación de algo ya anulado contesta 200 con yaEstabaInvalidado: true. La respuesta trae además archivoEvento (el evento sellado, para archivarlo en ruta junto al original) y avisoCliente (el estado del correo «Documento invalidado» que Facta manda solo cuando la empresa tiene el envío automático activo); vea la guía «Anular un documento sellado».

  • archivoEvento es { nombre, ruta, contenido } o null: el evento sellado en el formato del Archivo DTE, <CODIGO>.invalidacion.json. invalidationEventFileOf(anulado) lo devuelve validado, o null.
  • storage es un ManagedInvalidationReceipt, { destination, environment, invalidation, operationId }, o null. Es la copia de ese único archivo en Almacenamiento Facta, sin partes json ni pdf; si falló, lo dice invalidation.state.
  • avisoCliente trae { estado, destino, motivo? } del correo al cliente. Nunca hace fallar la anulación.
  • Copias en sus destinos. Con unlockKey (y runtime.replicate distinto de false), invalidate() e invalidateAndArchive() escriben archivoEvento.contenido en cada destino S3-compatible o Supabase Storage sincronizado, exactamente en archivoEvento.ruta, y avisan a Facta con kind: "invalidation". Si el servidor es anterior y no lo anuncia, el archivo se escribe igual y el aviso se omite. El resultado queda en eventCopies (por destino, report en reported, failed o skipped) y en sdkWarnings; nunca es un error. Los destinos que necesitan a una persona (Drive, OneDrive, FTP/SFTP) salen como byos_not_replicated.
typescript
const generationCode = "7875BC7A-9580-441D-94E4-FA455E9D8BD0";
const anulado = await facta.invalidate(generationCode, {
  tipoAnulacion: 1,
  motivo: "El precio unitario iba sin el descuento pactado",
  codigoGeneracionReemplazo: "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
  responsable: { nombre: "Ana Rivas", tipoDocumento: "36", numDocumento: "06142803901121" },
  solicita: { nombre: "Beto Cruz", tipoDocumento: "13", numDocumento: "012345678" },
});

if (anulado.yaEstabaInvalidado) console.log("ya estaba anulado; no se mandó nada");

registerReturn(generationCode, request, options?)

Manda la llave de firma. Devuelve ReturnResult.

Registra el evento de retorno de una factura (01), de exportación (11) o de sujeto excluido (14) que Hacienda ya selló y que esta API emitió. Es un evento aparte, con su propio código y su propio sello; no gasta correlativo y, una vez sellado, no se deshace. Se pueden registrar varios sobre el mismo documento hasta sumar lo que se vendió: disponible, en la respuesta y en getDocumentStatus(), dice cuánto queda de cada línea. Las líneas se cuentan desde 1, como en la factura impresa, y cada item lleva cantidad (unidades) o noGravado (cargo o abono que no afecta la base), una sola. Vea la guía Registrar un evento de retorno.

typescript
const retorno = await facta.registerReturn(codigoGeneracion, {
  items: [{ linea: 1, cantidad: 1 }],
}, { idempotencyKey: "retorno-1042-a" });

if (retorno.estado === "firmado") {
  // Hacienda no contestó (HTTP 202). Repita con la MISMA idempotencyKey y el
  // mismo cuerpo: se reenvía el mismo evento firmado.
  console.warn(retorno.detalle);
} else {
  console.log(retorno.selloRecibido, retorno.disponible);
}

invalidateAndArchive(generationCode, request, options)

Envía una anulación y guarda su evento en el journal cifrado de FileInvoiceArchive, separado de los artefactos de la factura original. Persiste la solicitud y la clave de idempotencia antes de llamar a la API. La llave de firma es necesaria en el primer envío o en una recuperación segura; use la misma operationId y idempotencyKey para reanudar una operación.

typescript
const generationCode = "7875BC7A-9580-441D-94E4-FA455E9D8BD0";
const invalidacion = await facta.invalidateAndArchive(generationCode, {
  tipoAnulacion: 1,
  motivo: "El precio unitario iba sin el descuento pactado",
  codigoGeneracionReemplazo: codigoGeneracionReemplazo,
  responsable: { nombre: "Ana Rivas", tipoDocumento: "36", numDocumento: "06142803901121" },
  solicita: { nombre: "Beto Cruz", tipoDocumento: "13", numDocumento: "012345678" },
}, {
  archive,
  operationId: cancellation.id,
  idempotencyKey: cancellation.id,
});

if (invalidacion.archive.state === "needs_attention") {
  console.error("la anulación pudo completarse; revise el journal antes de reintentar");
}

recoverInvalidation(operationId, archive?, signal?) y listPendingInvalidations(archive?)

recoverInvalidation() usa la solicitud y la clave guardadas. listPendingInvalidations() devuelve resúmenes sin exponer la solicitud ni el JWS. El SDK conserva el JWS exacto dentro del journal cifrado y verifica su hash al leerlo. La recuperación solo vuelve a enviar la misma solicitud durante una ventana conservadora de 23 horas. Si ya pasó esa ventana o la API responde que el DTE ya estaba anulado sin entregar el JWS del evento, recoverInvalidation() lanza operation_outcome_unknown; concilie el estado fiscal antes de volver a actuar.

downloadDocument(generationCode, kind?, options?)

No manda la llave de firma. Devuelve DownloadedDocument.

Devuelve los bytes exactos de json, pdf, ticket o invalidation (desde la 0.6.0: el evento de anulación sellado de ese documento, <CODIGO>.invalidacion.json, solo del Almacenamiento Facta; implica source=managed), sin parsearlos ni volver a serializarlos. El tipo predeterminado es json; exige el alcance download. El ticket se genera desde un DTE ya sellado y admite paperWidthMm entero de 40 a 120 (80 por defecto), sin emitir de nuevo. Esta regeneración está disponible para DTE emitidos por la API, no para los que se emitieron desde la app web. El área de retención dura una hora desde la firma, pero pasada esa hora la ruta sigue contestando: el documento se rearma desde la reserva, que guarda el JWS sellado.

typescript
const archivo = await facta.downloadDocument(dte.codigoGeneracion, "json");

await writeFile(archivo.filename ?? `${dte.codigoGeneracion}.json`, archivo.bytes);
console.log(archivo.contentType);   // "application/json"

No manda una solicitud a la API ni confirma una impresión física. Entrega los bytes de un PDF de carta o ticket al transporte que proporciona la aplicación integradora y devuelve su estado (submitted o unknown). No reintenta trabajos con resultado incierto.

El SDK acepta documentos pdf o ticket; el JSON legal no se puede enviar a una impresora como PDF. La impresora, su descubrimiento, el spooler y la reconciliación de trabajos inciertos son responsabilidad del adaptador del integrador. Para tickets se conserva el ancho del documento descargado (80 mm por defecto, o el ancho pedido al descargarlo).

typescript
const ticket = await facta.downloadDocument(dte.codigoGeneracion, "ticket", {
  paperWidthMm: 58,
});
const result = await facta.print(ticket, printerTransport);

if (result.state === "unknown") {
  // Reconcile with the printer or spooler before asking the user to retry.
}

listHolding(limit?)

No manda la llave de firma. Devuelve HoldingPage.

Qué hay en su área de retención ahora mismo. Evidencia de estado —dónde aterrizó, intentos, cuándo vence, cuántas veces se descargó—, nunca rutas de bucket ni contenido.

typescript
const pagina = await facta.listHolding(50);

for (const fila of pagina.documentos) {
  console.log(fila.codigoGeneracion, fila.whereLanded, fila.expiresAt);
}

getContract()

No manda la llave de firma. Devuelve el contrato OpenAPI.

El contrato que publica la propia API. El endpoint público no requiere una llave; si llama a getContract() desde una instancia Facta, el cliente adjunta su apiKey como en las demás peticiones.

typescript
const contrato = await facta.getContract();
console.log(contrato.info?.version);

FactaError

FactaErrorCode
unauthorized
invalid_api_key
key_revoked
key_expired
key_inactive
forbidden_scope
dte_type_not_allowed
ip_not_allowed
environment_not_allowed
sign_key_required
sign_key_invalid
sign_vault_locked
sign_vault_missing
invalid_request
validation_failed
not_found
method_not_allowed
idempotency_key_required
idempotency_key_reuse
idempotency_in_flight
prepare_token_invalid
rate_limited
amount_limit
mh_rejected
mh_unreachable
correlative_unavailable
service_unavailable
no_storage_destination
storage_unsupported
storage_unavailable
storage_contract_invalid
entrega_vencida
entrega_token_invalido
canal_no_marcado
internal_error
operation_outcome_unknown
archive_integrity_error
network_error

Los errores HTTP de Facta y de red se representan como FactaError; sus códigos son la interfaz estable para decidir cómo continuar. También pueden ocurrir errores locales de JavaScript/Web Crypto al validar opciones, abrir un vault o usar el archivo. Los adaptadores de proveedores tienen sus propias clases de error. No convierta todo catch en una decisión sobre FactaError: compruebe el tipo y vuelva a lanzar lo que su integración no reconozca.

PropiedadTipoQué es
codeFactaErrorCodeCódigo estable del servidor o uno de los códigos locales descritos abajo.
statusnúmeroEl HTTP. 0 cuando el fallo ocurrió antes de salir.
messagetextoEspañol, para el humano que lee el registro. Puede reescribirse: un cliente que haga switch sobre el mensaje se rompe con una corrección de ortografía.
detailsdesconocidoDepende del código. validation_failed trae issues; rate_limited, la ventana y los segundos.
isRejectionsí/no«Hacienda leyó el documento y lo negó», frente a «no llegamos a Hacienda». Es la diferencia que decide qué hacer después.
spentobjeto o nullEl correlativo que este fallo ya gastó, cuando lo hay. El §167 permite corregir con ese mismo número.
mhObservationslista de textoLo que dijo Hacienda, palabra por palabra, cuando rechazó.

Estos códigos son propios del SDK y nunca aparecen en la tabla HTTP de la API:

Código localSignificado y siguiente paso
network_errorLa petición no recibió respuesta; reintente solo con la misma clave de idempotencia.
operation_outcome_unknownNo se pudo recuperar el evento de anulación de forma segura; concilie el estado fiscal antes de operar de nuevo.
storage_contract_invalidLa capacidad, el recibo o los bytes contradicen el contrato; conserve el éxito fiscal y concilie el almacenamiento.
archive_integrity_errorEl journal cifrado no pasó la verificación de identidad o hash; conserve una copia y revise antes de repararlo.
typescript
import { FactaError } from "@facta-dte/api";

try {
  await facta.issue(venta);
} catch (error) {
  if (!(error instanceof FactaError)) throw error;

  switch (error.code) {
    case "mh_rejected":
      console.error(error.spent?.numeroControl, error.mhObservations);
      break;
    case "no_storage_destination":
      break;
    case "validation_failed":
      console.error(error.details);
      break;
    default:
      throw error;
  }
}

El SDK elimina los campos de credenciales de error.details, redacta las llaves configuradas si aparecen en texto y usa un mensaje genérico para fallos de red. Aun así, trate cualquier dato de error como información operativa y no registre solicitudes o documentos completos.

Reintentos y contingencia

El cliente reintenta solo donde reintentar es seguro, con espera creciente: idempotency_in_flight (la primera petición sigue trabajando), service_unavailable, correlative_unavailable, mh_unreachable y network_error. Nunca en un 4xx y nunca en un rechazo.

La Idempotency-Key se acuña una vez, fuera del bucle. Generar una nueva por intento es exactamente el error que esa cabecera existe para evitar: quemaría un segundo correlativo.

Y un 202 no es un error, así que no pasa por aquí: llega como un resultado con estado: "contingencia". Lo cuenta entera la guía Contingencia y resultados inciertos.

Tipos

Los tipos se leen del contrato TypeScript (mod.ts), no de un .d.ts generado. Los que se usan a diario:

  • DteRequest, LineItem, Recipient — lo que se manda.
  • IssueResult = SealedDte | DteInContingency — la unión que obliga a mirar el estado antes de leer totales.
  • PreparedDte, DocumentStatus, InvalidationResult, DtePage, HoldingPage.
  • Status, RateLimitWindow — lo que devuelve status().
  • FactaError, FactaErrorCode, SpentCorrelative.
  • DeliverOptions, DeliveryOffer, DeliveryStatus, DeliveryChannelResult, WaitedDelivery — la entrega por correo y WhatsApp.
  • RemoteCopyRecord, ArchiveWarning — el resultado de las copias en sus destinos y sus advertencias.
  • ReturnRequest, ReturnResult (ReturnSealed | ReturnPending), ReturnAvailability, ReturnSummary — desde la 0.4.0.

Lo que este cliente no hace

  1. No calcula dinero. Ni IVA, ni retenciones, ni totales, ni el número de control, ni fechas fiscales. Todo eso lo produce el servidor y el cliente lo transporta. Un SDK que calcule dinero es un segundo motor fiscal, y dos motores se desincronizan el primer martes.
  2. No firma. El certificado no pasa por aquí en ningún momento. Lo que sí pasa, en las rutas que firman, es X-Facta-Sign-Key: la contraseña que abre el vault en el servidor. El cliente la reenvía y no hace nada con ella.
  3. No elige su almacenamiento por usted. FileInvoiceArchive guarda copias cifradas recuperables en el proceso integrador y los adaptadores de destinos replican archivos a proveedores compatibles. El servidor puede conservar una copia Facta y devolver su recibo; downloadDocument() solo le entrega los bytes y no crea una copia local por usted. La impresora requiere un transporte que usted proporciona y el SDK no afirma que un archivo quedó impreso. El correo y el WhatsApp los entrega Facta cuando usted los pide con deliverEmail() o deliverWhatsApp(); el estado de cada canal es el que Facta informa, y emitir nunca depende de él.

Una prueba de arquitectura del propio paquete falla si alguna de las dos primeras deja de ser cierta.

Opciones de archivo inline

issueAndArchive conserva el JSON firmado y PDF exactos recibidos al emitir y no vuelve a descargarlos. Las respuestas anteriores solo usan descarga para artefactos faltantes. includeTicket:false conserva JSON/PDF/JWS sin pedir un ticket; no lo combine con ticketPaperWidthMm explícito. El valor predeterminado sigue siendo includeTicket:true. La recuperación conserva esa elección.

Para comprobar una copia Facta mientras aún existe retención temporal, use downloadDocument(generationCode, "pdf", { source: "managed" }) (también admite json). Exige storageSource === "managed" y no usa retención como alternativa. Esta opción rechaza tickets antes de HTTP. "invalidation" siempre usa el origen administrado. Si un servidor anterior no prueba el origen, devuelve storage_unsupported; un origen contradictorio produce storage_contract_invalid. La descarga predeterminada conserva su orden.

Escriba para buscar Por ejemplo: idempotencia, contingencia, 422, emitir.

↑ ↓ moverse Enter abrir Esc cerrar