> ## 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.

# Evitar comprobantes duplicados

> Guardá cada intento para que un reintento no emita otro comprobante en ARCA.

A diferencia de Stripe, ARCA no recibe una clave de idempotencia. Si una
respuesta se pierde, ARCA no puede distinguir un reintento de una emisión
nueva.

`facturas` resuelve ese problema con una clave estable de tu aplicación y un
almacenamiento compartido, llamado `store` en la API. El SDK guarda ahí el
número reservado, el input y el resultado. Cuando repetís la misma operación
con la misma `idempotencyKey`, consulta esa reserva en vez de empezar otra.

Podés usar el Postgres o Redis que tu aplicación ya tiene. También hay
adaptadores para un directorio privado y para memoria. El SDK no instala una
base de datos ni un servicio externo.

## Qué guarda cada opción

| Store                       | Reservas persistentes                       | Secuencia coordinada                                  |
| --------------------------- | ------------------------------------------- | ----------------------------------------------------- |
| Postgres                    | sí                                          | sí, con una fila de bloqueo y un `UPDATE` condicional |
| Redis                       | sí                                          | sí, con `SET NX PX` y una liberación verificada       |
| Archivos                    | sí, en un solo servidor con volumen privado | sí, con un directorio de bloqueo y vencimiento        |
| Memoria                     | no. No sobrevive a un reinicio              | solo dentro del proceso                               |
| Store propio con `withLock` | según tu almacenamiento                     | sí, con tu implementación                             |
| Store propio sin `withLock` | según tu almacenamiento                     | no. Cada proceso lee y escribe por su cuenta          |

Con un store que provee `withLock`, `issue()`, `issueCreditNote()` e
`issueDebitNote()` coordinan la numeración. Dos llamadas simultáneas para el
mismo punto de venta y tipo de comprobante reciben números consecutivos y cada
una escribe una sola vez. Sin `withLock`, otros procesos que usan ese punto de
venta quedan fuera de la coordinación.

Antes de reservar un número, el SDK revisa si la operación anterior quedó sin
resolver. Primero consulta ARCA. Si todavía no puede saber qué pasó, devuelve
`indeterminate` con `lookup: { kind: "blocked", by: <clave> }` sin reservar ni
emitir nada. Conciliá esa clave con `recover()` y repetí la llamada.

El bloqueo se renueva mientras el proceso está activo. Si el proceso se cae, el
bloqueo vence, pero la reserva pendiente sigue frenando la secuencia. Así, otro
proceso puede continuar sin reutilizar a ciegas un número cuyo resultado todavía
no se conoce.

## Postgres

Usá el cliente que ya tiene tu aplicación. Neon, Supabase Postgres, Vercel
Postgres, `pg` y `postgres` pueden proveer la función de consulta
parametrizada. Los resultados pueden ser una lista de filas o `{ rows }`. Con
`postgres`, adaptá `sql.unsafe(text, params)`. Creá la tabla por defecto una
sola vez:

```sql theme={null}
CREATE TABLE arca_store (
  key text PRIMARY KEY,
  value text NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now()
);
```

```ts theme={null}
const store = createPostgresStore({
  query: (text, params) => sql.query(text, params),
  table: "arca_store", // Identificador SQL simple opcional.
});
```

La creación atómica usa `INSERT ... ON CONFLICT DO NOTHING RETURNING key`. El
adaptador no crea la tabla. El bloqueo de secuencia es una fila más de esa tabla,
tomada con el mismo `INSERT` y liberada con un `DELETE` que verifica el dueño,
y una fila abandonada se recupera con un `UPDATE` condicional sobre
`updated_at`. No usa bloqueos de sesión, así que funciona detrás de PgBouncer en
modo transacción.

## Redis

```ts theme={null}
import { createRedisStore } from "facturas";
const store = createRedisStore(redis);
// Para Upstash: createRedisStore(redis, { flavor: "upstash" });
```

Un cliente con `call` usa ioredis `SET key value NX`. Si no, el adaptador usa
Upstash `set(key, value, { nx: true })`. Usá un Redis persistente, sin desalojo de
las claves de reserva: los registros no llevan TTL. El bloqueo de secuencia sí es
una clave con vencimiento, tomada con `SET NX PX`, renovada mientras se la
tiene y borrada solo si sigue siendo tuya. Necesita `del` en el cliente: sin
`del`, el adaptador no expone `withLock` y la secuencia no se coordina.

## Archivos

```ts theme={null}
import { createFileStore } from "facturas";
const store = createFileStore("/private/durable/arca");
```

Usá un volumen privado y persistente en un único servidor. Cada clave se
transforma en un nombre de archivo. La creación es exclusiva y el reemplazo usa
un archivo temporal y `rename`. Los archivos quedan con modo `0600` y los
directorios
nuevos con `0700`. El bloqueo de secuencia es un directorio `.lock` creado con
`mkdir`, con el dueño y el vencimiento adentro: coordina varios procesos sobre
el mismo volumen, no varios servidores sobre un NFS compartido.

## Memoria

```ts theme={null}
import { createMemoryStore } from "facturas";
const store = createMemoryStore();
```

Para pruebas y ejemplos. Serializa los refrescos de ticket y la secuencia
dentro del objeto compartido, pero **no sobrevive a un reinicio**. Si el proceso
se reinicia entre llamadas, no puede deduplicarlas.

## Implementar otro store

```ts theme={null}
type ArcaStore = {
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  add(key: string, value: string): Promise<boolean>;
  delete?(key: string): Promise<void>;
  withLock?<T>(key: string, fn: () => Promise<T>): Promise<T>;
};
```

`add` tiene que devolver false de forma atómica sin cambiar un valor existente.
El `withLock` opcional coordina los refrescos de ticket WSAA y la secuencia del
punto de venta. Tiene que ser exclusivo entre procesos y soltarse siempre, aun
si el proceso muere. Un store sin `withLock` conserva las reservas y la
recuperación, pero no coordina la secuencia entre procesos. Cuando pasás las dos
opciones, un `wsaaSessionStore`
explícito gana para los tickets.

<Accordion title="Formato y vida de los registros">
  Las claves usan `arca:v1:wsaa:{environment}:{service}:{fingerprint}` y
  `arca:v1:attempt:{environment}:{taxId}:{idempotencyKey}`. Los registros de
  reserva contienen el hash del input, la operación, las coordenadas reservadas y
  el input exacto enviado. Contienen datos fiscales y de clientes. Restringí el
  acceso y protegé los backups.

  `arca:v1:settled:{environment}:{taxId}:{idempotencyKey}` guarda un resultado
  que tiene que sobrevivir al reintento, y se escribe una sola vez con `add`. Con
  `{ v: 1, kind: "conflict", number, found, settledAt }`, otro comprobante ocupa
  el número reservado. Una repetición con esa clave o un `recover()` devuelven el
  mismo `conflict` sin consultar al proveedor. Con
  `{ v: 1, kind: "superseded", number, by, settledAt }`, la barrera probó que el
  número estaba vacío y se lo dio a la clave `by`. La clave vieja consulta el
  número una vez y nunca reenvía. Devuelve `indeterminate` con
  `lookup: { kind: "superseded", by }` si el número sigue vacío o si el
  comprobante que hay ahí es de `by` o de una clave que a su vez se lo quitó a
  `by`. Emití bajo una clave nueva. Devuelve `conflict` solo si alguna clave de
  esa cadena anotó un conflicto en ese número, porque entonces un desconocido
  llegó al número y el comprobante se atribuye a mano. Las autorizaciones no se
  anotan porque ARCA es la fuente de verdad y cada repetición la consulta. Los
  rechazos tampoco, porque el input se corrige bajo una clave nueva. Si el store
  falla al anotar el conflicto,
  la llamada lanza `ArcaConfigurationError` en vez de devolver un conflicto que
  un reintento podría no volver a ver.

  Cada registro lleva su versión. `v: 1` es una reserva de WSFE sin detalle y la
  lee cualquier versión desde la 0.9. `v: 2` es una reserva de WSMTXCA o con
  detalle de ítems. Siempre nombra su proveedor, y la 0.10 no puede reproducirla,
  justamente para que volver a la versión anterior no reenvíe por WSFE un comprobante que era de
  WSMTXCA. Preservá las dos versiones.

  `arca:v1:sequence:{environment}:{taxId}:{salesPoint}:{voucherType}` guarda la
  última reserva reclamada en esa secuencia y si ARCA ya informó su desenlace. Se
  escribe antes que la reserva que nombra.
  `arca:v1:lock:sequence:...` es la clave del bloqueo. Los dos son registros de
  coordinación, no evidencia fiscal. Se reescriben en cada reclamo y se pueden
  borrar si hacen falta, siempre que ninguna reserva quede sin conciliar. Repetir
  una clave con otro input, otra
  operación, otro proveedor u otro `number` explícito es una incompatibilidad de
  idempotencia y lanza `ARCA_INPUT_IDEMPOTENCY_MISMATCH`.

  **No podés borrar, vencer ni reescribir los registros de reserva.** El SDK solo
  los crea, nunca guarda resultados encima y siempre consulta a ARCA en una
  repetición. Borrar una reserva puede hacer que un reintento posterior emita
  otra factura.
</Accordion>
