Skip to main content
issue() es el camino normal para emitir. Recibe los datos de negocio, arma el request fiscal y trata las respuestas ambiguas de ARCA. Si necesitás controlar un request o una numeración que issue() no cubre, usá los métodos directos de ARCA.

Emitir

Definí las variables de entorno. No hace falta store ni ningún servicio externo. Este bloque es examples/primera-factura.ts:

Evitar duplicados al reintentar

ARCA no recibe una clave de idempotencia como Stripe. Si la conexión se corta después del envío, repetir la llamada sin guardar el intento puede duplicar la factura. facturas facilita la deduplicación con un store: guarda el número reservado, el input y el resultado. Pasale el ID estable de tu venta como idempotencyKey. Este ejemplo supone que venta es tu venta y que ya creaste la tabla Postgres.
Una clave tiene de 1 a 255 caracteres. Usá el ID de la venta o del pedido, nunca un UUID nuevo por intento. No pongas CUIT, DNI ni otros datos personales en las claves. Reusá la misma clave y el mismo input en los reintentos. Si el input cambia, se lanza ARCA_INPUT_IDEMPOTENCY_MISMATCH. Las claves están alcanzadas al CUIT del cliente y al entorno. representedTaxId también se controla como parte de la identidad del input. Una clave sin store lanza antes de cualquier I/O con el proveedor. Claves distintas identifican operaciones de negocio distintas. Con un store que provee withLock, como Postgres, Redis, archivos, memoria o el tuyo, las llamadas con clave sobre el mismo punto de venta y tipo de comprobante se serializan: cada una toma el número siguiente y escribe una sola vez. La coordinación alcanza a los procesos que comparten ese store, no a otros escritores del mismo punto de venta. El detalle, con la tabla de garantías por adaptador, está en Evitar comprobantes duplicados. signal marca el límite de tiempo con un AbortSignal, por ejemplo AbortSignal.timeout(20_000). Aborta el login WSAA, la escritura y las consultas de esa llamada. Si se corta después de haber enviado la escritura, el resultado es indeterminate con lookup.kind === "aborted": la reserva queda y recover() la concilia. No hay un timeoutMs aparte: un solo signal compone con el que ya tenga tu aplicación. El ejemplo completo, con el tratamiento de los cuatro resultados, está en examples/issue-invoice.ts. recover(clave, opciones) concilia sin emitir. Solo consulta la reserva guardada, con el proveedor y el número que quedaron registrados: nunca autoriza ni reserva un número nuevo. Si ARCA confirma que el número está vacío, el resultado es indeterminate con lookup.kind === "not_found", no una autorización. Para emitir, llamá a issue() con la misma clave. Si no hay reserva para esa clave, lanza ArcaInputError con code === "ARCA_INPUT_RESERVATION_NOT_FOUND". Esa ausencia no es el lookup.kind === "not_found" de una reserva consultada. Tampoco prueba que otro proceso todavía no esté por guardarla. Acepta representedTaxId, forceRefresh, include y signal.

Revisá antes de emitir

preview() deriva exactamente lo que enviaría issue() sin hacer I/O. No usa el store, WSAA, SOAP ni lee el próximo número. Lanza los mismos errores de input que lanza issue() antes de su primera llamada, así que un input que previsualiza limpio no genera ningún error local nuevo al emitir. Este bloque es examples/preview.ts:
preview() es sincrónico y devuelve la voucherClass derivada, el voucherType, los mismos amounts que informa un resultado autorizado, y request, el input que enviaría issue(). Es un WsfeVoucherInput o el request de WSMTXCA si pasás { service: "wsmtxca" }. El número de comprobante no aparece porque recién se conoce cuando se reserva el número al emitir. Previsualizar una nota sí necesita el original, así que previewCreditNote() y previewDebitNote() son asincrónicas y hacen esa consulta. Están en Notas de crédito.

Datos de la factura

El emisor es tu afirmación legal en cada llamada. El SDK nunca lo infiere de los ítems ni del Padrón. Un emisor RI produce A para receptores RI o Monotributo y B para las demás condiciones soportadas. Los emisores Monotributo, Exento y No Alcanzado producen C y usan items: [{ amount: 10_000 }]. ARCA valida la habilitación real. to es el receptor fiscal con su documento y condición ante el IVA, no un registro de cliente. Los importes son enteros en centavos. Para ítems de RI, elegí net o gross en cada ítem y uno de 0 | 2.5 | 5 | 10.5 | 21 | 27 | "exempt" | "untaxed" para vat. El cero numérico es una alícuota de IVA. Los importes exentos y no gravados tienen campos fiscales propios. Los ítems se agrupan por alícuota antes del redondeo Round Half Even. total, cuando lo pasás, afirma el total enviado. El SDK ajusta el IVA de cabecera solo dentro de un centavo por cada alícuota numérica emitida, y mantiene el IVA no negativo. Los totales de clase C tienen que coincidir exactamente. Los resultados autorizados exponen computedTotal, sentTotal y vatAdjustment en voucher.amounts, todos en centavos. Los valores por defecto son la fecha de hoy en Buenos Aires, productos (concepto 1) y ARS con tipo de cambio 1. Usá currency: "USD" con un exchangeRate decimal positivo en string, o service: { from, to, dueDate } para el concepto 2. Las fechas aceptan YYYY-MM-DD o YYYYMMDD. El fin del servicio tiene que ser igual o posterior a su inicio y el vencimiento de pago igual o posterior a la fecha de la factura. Los receptores que no son consumidor final requieren un cuit de 11 dígitos. Un consumidor final acepta un cuit o un dni, o ninguno por debajo del umbral de identificación. Desde ARS 10.000.000 (incluidos los USD convertidos al tipo de cambio informado), la identificación es obligatoria según la RG 5866/2026. Cuando el cliente pide el CUIT para deducir en Ganancias, informalo sin importar el monto. Los controles de forma del documento no verifican la inscripción ante el proveedor. family elige la familia del comprobante y por defecto es "ordinary" (1, 6, 11). "retention_legend" es A con leyenda y existe solo en clase A (51, 52, 53). "fce" es Factura de Crédito Electrónica MiPyME (201 a 213): requiere dueDate y fce: { cbu, alias?, transfer?, reference? }, con un CBU de 22 dígitos. ARCA valida la cuenta bancaria real y tu habilitación. taxes son tributos y percepciones. Cada fila es { id, description?, base, rate, amount }, con base y amount en centavos y rate en porcentaje. Los tributos quedan fuera de la aritmética de los ítems y suman al total. amounts es un desglose fiscal ya revisado y es excluyente con items: el SDK no recalcula el IVA ni lo ajusta. Toma { net, vat, exempt?, untaxed?, vatRates? } en centavos, con filas vatRates de la forma { id, base, amount }. El total opcional afirma el total revisado: se controla contra las reglas de conciliación de importes del proveedor y no se reescribe en silencio. concept: "products_and_services" es el concepto 3, junto a "products" y "services". dueDate informa el vencimiento de pago. paidInForeignCurrency marca la cancelación en la misma moneda extranjera y no se acepta en ARS. Para monedas que no son ARS ni USD, pasá currency: { id: "060" } con un exchangeRate explícito. ARCA controla la elegibilidad. to.condition también acepta el número de la condición de IVA del receptor del catálogo de ARCA, con cuit, dni o document: { type, number }. Los campos optionalFields, buyers y activities pasan tal cual a WSFE y a WSMTXCA. No dupliques ahí lo que ya informa fce. Para el detalle de ítems de WSMTXCA, mirá WSMTXCA. El ejemplo compilado de todo esto es examples/emision-completa.ts.

Qué hace issue() al emitir

Sin clave, una llamada lee un próximo número y autoriza una vez, con a lo sumo una consulta de identidad después de una respuesta indeterminada. Es el comportamiento de la v0.8. Una primera llamada con clave reserva ese número antes de escribir. Una repetición con clave consulta la reserva: solo not_found habilita una autorización del número guardado. Un comprobante encontrado nunca se reenvía. Las escrituras indeterminadas y los rechazos 10016 con clave pueden agregar una consulta. issueCreditNote() agrega la consulta del original solo cuando crea una reserva nueva. Un 10016 sobre un número que esta misma llamada reservó no se resuelve por coincidencia de campos: si la consulta encuentra un comprobante el resultado es conflict, y si no encuentra nada sigue siendo rejected. Dos ventas con datos fiscales idénticos, por ejemplo dos ventas a consumidor final por el mismo importe el mismo día, coinciden en todos los campos. Por eso, la coincidencia probaría consistencia y nunca autoría. La comparación de identidad queda para una reserva que ya existía, el único caso en el que el número puede ser una escritura propia anterior. Todo conflict se anota en el store antes de responder. Una repetición con esa clave, o un recover(), devuelve el mismo conflict sin ninguna llamada al proveedor. Un indeterminate informa en lookup por qué quedó abierto: not_found, incomplete, failed, aborted cuando venció tu signal, blocked cuando otra reserva sin resolver todavía frena la secuencia y superseded cuando la secuencia siguió sin esta clave. En blocked no se reservó ningún número: by nombra la clave a conciliar con recover(). En superseded la clave by se llevó el número: esta clave nunca va a escribir, así que emití bajo una clave nueva. El segundo argumento acepta idempotencyKey, signal, representedTaxId, forceRefresh, service, number e include: { raw: true, exactInput: true }. Los resultados no traen la evidencia cruda por defecto. sent se incluye solo en resultados autorizados y solo si lo pedís. Una repetición sin un resultado de escritura observado usa un intento indeterminado con reason: "incomplete_response". La consulta aporta la evidencia de autorización. service elige el proveedor: "wsfe" por defecto, "wsmtxca" para el detalle de ítems. Nunca cambia solo. number es un número reservado por fuera: cuando lo pasás, la llamada no lee el próximo número, así que la aplicación sigue siendo dueña de la secuencia del punto de venta. issue() cubre la emisión de CAE para facturas y notas. No cubre CAEA, exportación ni los demás servicios de ARCA. El comparador de identidad compara coordenadas, fecha, concepto, receptor, moneda, todos los importes de cabecera, alícuotas de IVA, fechas de servicio, tributos, campos opcionales, compradores, actividades, las asociaciones de notas (incluidos el CUIT emisor y la fecha informados) y el flag de pago en moneda extranjera. Los campos faltantes quedan incompletos y nunca cuentan como prueba de coincidencia. Las diferencias son conflictos. Los campos directos que quedan fuera de ese conjunto se consideran incompletos.
Last modified on September 10, 2026