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.
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 usanitems: [{ 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.