Skip to main content
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

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:
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

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

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

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

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.
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.
Last modified on September 10, 2026