idempotencyKey with a durable store, and every call to issue() writes the reservation before it ever contacts ARCA. On retry, issue() finds the existing reservation and consults the already-reserved number instead of requesting a new one. The sale gets exactly one invoice, regardless of how many times the code runs.
How it works
1
First call: reserve and authorize
issue() reads the next available voucher number, writes a reservation entry to your store (bound to the idempotency key), and then sends the authorization request to ARCA. Even if the process crashes after the reservation is written but before ARCA responds, the number is preserved.2
Retry: look up the reservation
On the next call with the same key and the same input,
issue() finds the stored reservation and consults the already-reserved number. It never reads a new number or sends a duplicate authorization request.3
Outcome returned
The retry returns the same outcome shape (
authorized, rejected, indeterminate, or conflict) based on what ARCA says about the reserved number. Your code can handle it identically to the first call.Postgres example
Key rules
string
required
A stable, business-level identifier for the operation. Use the sale ID, order ID, or another identifier that is already unique in your system and stays the same across retries.
What makes a good idempotency key?
What makes a good idempotency key?
- Use your existing business IDs.
sale.id,order.id,payment.id— identifiers you already have and that remain constant for the lifetime of the operation. - Never use a fresh UUID per attempt. A new UUID on every retry defeats the purpose: each attempt looks like a new operation.
- Never include PII. Do not put CUIT, DNI, email addresses, or any personal data in a key. Keys are stored in your persistence layer and may appear in logs.
- Keep keys 1–255 characters long. Longer values throw
ArcaInputErrorbefore any I/O. - Keys are scoped to CUIT + environment. The same key string in production and in the sandbox is safe — they do not collide.
- Prefix by operation type when the same business object generates multiple documents. For example, use
nc:${devolucion.id}for a credit note so it doesn’t collide with the original invoice’s key.
Changing the input for an existing key
If you callissue() with the same idempotency key but a different input, the SDK throws ARCA_INPUT_IDEMPOTENCY_MISMATCH before any network I/O. This is intentional: a key is permanently bound to its input once the first call writes the reservation. The only correct retry is with the identical input.
Using a key without a store
Passing anidempotencyKey without configuring a store on the client throws ArcaInputError before any I/O with ARCA. The store is not optional when keys are in use.
Available stores
Postgres
Production-ready. Pass any
query function compatible with pg. Works with Vercel Postgres, Supabase, Neon, and plain node-postgres.Redis
Production-ready. Works with
ioredis and the redis npm package via the sendCommand adapter.File
Durable on a single machine. Useful for small deployments and local testing against real ARCA credentials.
Memory
In-process only. Does not survive restarts. Use for unit tests and local exploration — never in production.
Consulting a reservation without issuing
recover(key, options) looks up an existing reservation and consults ARCA about the stored voucher number — without authorizing or reserving anything new.
If ARCA reports the reserved number as empty,
recover() returns indeterminate with lookup.kind === "not_found". It does not authorize the voucher. To authorize, call issue() with the same key and the identical input. If no reservation exists for the key, recover() throws ArcaInputError.recover() accepts representedTaxId, forceRefresh, and include as options, matching the same options available on issue().