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

# Safe Invoice Retries with Idempotency Keys in facturas

> Pass an idempotencyKey and a store to arca.issue() so that retries after crashes consult the reserved number instead of issuing a duplicate.

Without an idempotency key, a network failure or process crash mid-call leaves the outcome ambiguous. The number may have been reserved, the authorization request may have reached ARCA, or the response may have been lost in transit. Retrying blindly in this situation risks issuing the same invoice twice — two authorized vouchers for the same sale, both permanent in ARCA's records.

Idempotency keys solve this. Pair an `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.

<Tip>
  Use idempotency keys and a durable store in every production application. They are optional for initial exploration but should be the default from your first real deployment onward.
</Tip>

## How it works

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Postgres example

```ts twoslash 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 venta = { id: "sale-abc-123", totalEnCentavos: 150_000 };

const factura = await arca.issue(
  {
    issuer: "monotributo",
    salesPoint: 3,
    to: { condition: "consumidor_final" },
    items: [{ amount: venta.totalEnCentavos }],
  },
  { idempotencyKey: venta.id },
);
```

## Key rules

<ParamField body="idempotencyKey" type="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.
</ParamField>

<Accordion title="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 `ArcaInputError` before 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.
</Accordion>

### Changing the input for an existing key

If you call `issue()` 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.

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

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

// First call — reservation written with items: [{ amount: 150_000 }]
await arca.issue(
  { issuer: "monotributo", salesPoint: 3, to: { condition: "consumidor_final" }, items: [{ amount: 150_000 }] },
  { idempotencyKey: "sale-abc-123" },
);

// Retry with a different amount — throws ARCA_INPUT_IDEMPOTENCY_MISMATCH
await arca.issue(
  { issuer: "monotributo", salesPoint: 3, to: { condition: "consumidor_final" }, items: [{ amount: 200_000 }] },
  { idempotencyKey: "sale-abc-123" }, // same key, different input
);
```

### Using a key without a store

Passing an `idempotencyKey` 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

<CardGroup cols={2}>
  <Card title="Postgres" icon="database">
    Production-ready. Pass any `query` function compatible with `pg`. Works with Vercel Postgres, Supabase, Neon, and plain `node-postgres`.
  </Card>

  <Card title="Redis" icon="server">
    Production-ready. Works with `ioredis` and the `redis` npm package via the `sendCommand` adapter.
  </Card>

  <Card title="File" icon="file">
    Durable on a single machine. Useful for small deployments and local testing against real ARCA credentials.
  </Card>

  <Card title="Memory" icon="microchip">
    In-process only. Does not survive restarts. Use for unit tests and local exploration — never in production.
  </Card>
</CardGroup>

See [Guides → Stores](/guides/stores) for setup instructions, schema migrations, and how to implement a custom store.

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

```ts twoslash 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 result = await arca.recover("sale-abc-123");
```

<Note>
  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`.
</Note>

`recover()` accepts `representedTaxId`, `forceRefresh`, and `include` as options, matching the same options available on `issue()`.
