> ## Documentation Index
> Fetch the complete documentation index at: https://facturas-sdk.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Facturas

> Emití y previsualizá facturas, y tratá cada resultado fiscal sin perder comprobantes.

`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](/reference/arca-services).

## Emitir

Definí las [variables de entorno](/reference/configuration#variables-de-entorno).
No hace falta store ni ningún servicio externo. Este bloque es
[examples/primera-factura.ts](https://github.com/LaPyme/facturas/blob/main/examples/primera-factura.ts):

```ts theme={null}
import { createArcaClient } from "facturas";

const arca = createArcaClient();

const factura = await arca.issue({
  issuer: "monotributo",
  salesPoint: 3,
  to: { condition: "consumidor_final" },
  items: [{ amount: 150_000 }], // ARS 1.500,00 en centavos
});
```

## 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](/guides/avoid-duplicates#postgres).

```ts theme={null}
import { createArcaClient, createPostgresStore } from "facturas";
import { sql } from "@vercel/postgres";

const arca = createArcaClient({
  store: createPostgresStore({ query: (text, params) => sql.query(text, params) }),
});

const factura = await arca.issue(input, { idempotencyKey: venta.id });
```

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](/guides/avoid-duplicates#qué-guarda-cada-opción).

`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](https://github.com/LaPyme/facturas/blob/main/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](https://github.com/LaPyme/facturas/blob/main/examples/preview.ts):

```ts theme={null}
import { createArcaClient, createMemoryStore, type IssueInput } from "facturas";

// Solo para este ejemplo. En una aplicación, usá un store persistente.
const arca = createArcaClient({ store: createMemoryStore() });
const venta = { id: "sale-example-002", totalEnCentavos: 121_000 };

const input: IssueInput = {
  issuer: "responsable_inscripto",
  salesPoint: 3,
  to: { condition: "consumidor_final" },
  items: [{ gross: 121_000, vat: 21 }], // ARS 1.210,00 en centavos
};

// preview() es sincrónico y no consulta el store, WSAA ni SOAP.
const previsualizacion = arca.preview(input);
console.log(
  previsualizacion.voucherClass, // "B"
  previsualizacion.voucherType, // 6
  previsualizacion.amounts, // computedTotal, sentTotal y vatAdjustment
  previsualizacion.request // El input de WSFE, sin el número de comprobante.
);

if (previsualizacion.amounts.sentTotal !== venta.totalEnCentavos) {
  throw new Error("El total de la factura no coincide con el de la venta.");
}

const factura = await arca.issue(input, { idempotencyKey: venta.id });
if (factura.kind === "authorized") {
  // Los importes emitidos son los que mostró la vista previa.
  console.log(factura.voucher.amounts, previsualizacion.amounts);
}
```

`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](/guides/credit-notes#vista-previa-de-notas).

## 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](https://www.argentina.gob.ar/normativa/nacional/norma-427092/texto).
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](/guides/wsmtxca). El ejemplo
compilado de todo esto es
[examples/emision-completa.ts](https://github.com/LaPyme/facturas/blob/main/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.

| Resultado       | Qué significa y qué hacer                                                                                                                                             |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authorized`    | Guardá el comprobante y el CAE. `recoveredByMatch: true` significa que el input guardado coincidió con la identidad consultada. Esto prueba consistencia, no autoría. |
| `rejected`      | Revisá los `issues` de ARCA. Una clave queda ligada a su input incluso después de un rechazo.                                                                         |
| `indeterminate` | Conservá el número y la evidencia. Conciliá o repetí el input idéntico con su clave existente.                                                                        |
| `conflict`      | Hay otro comprobante en el número reservado. Detené el flujo e investigá.                                                                                             |

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.
