# Idempotencia

> Repita cualquier solicitud sin miedo: con la misma Idempotency-Key nada se hace dos veces, ni se timbra, ni se crea, ni se manda un correo.

Una red que se corta a la mitad deja a su sistema sin saber si la factura se timbró. Por eso todo lo que timbra exige el encabezado `Idempotency-Key`: si repite la solicitud con la misma llave, Fiscalisto le contesta el comprobante que ya timbró en vez de timbrar otro.

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/invoices \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Idempotency-Key: pedido-A-1042" \
  -H "Content-Type: application/json" \
  -d '{ "customer": "cus_OufpKqnrvx9p1", "payment_form": "04", "items": [{ "product": "prd_OufpKqnrvx9p1" }] }'
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

// La llave viene de FISCALISTO_API_KEY.
const fiscalisto = new Fiscalisto();

const invoice = await fiscalisto.invoices.create({
  customer: "cus_OufpKqnrvx9p1",
  payment_form: "04",
  items: [{ product: "prd_OufpKqnrvx9p1" }],
}, { idempotencyKey: "pedido-A-1042" });
```

```python tab="Python"
from fiscalisto import Fiscalisto

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

invoice = fiscalisto.invoices.create({
    "customer": "cus_OufpKqnrvx9p1",
    "payment_form": "04",
    "items": [{"product": "prd_OufpKqnrvx9p1"}],
}, idempotency_key="pedido-A-1042")
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
$fiscalisto = new Fiscalisto();

$invoice = $fiscalisto->invoices->create([
    'customer' => 'cus_OufpKqnrvx9p1',
    'payment_form' => '04',
    'items' => [['product' => 'prd_OufpKqnrvx9p1']],
], ['idempotency_key' => 'pedido-A-1042']);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	invoice, err := client.Invoices.Create(ctx, &fiscalisto.InvoiceCreate{
		Customer:    fiscalisto.IDOf[fiscalisto.InvoiceCreateCustomer]("cus_OufpKqnrvx9p1"),
		PaymentForm: fiscalisto.String("04"),
		Items: []fiscalisto.InvoiceCreateItem{
			{Product: fiscalisto.String("prd_OufpKqnrvx9p1")},
		},
	}, fiscalisto.WithIdempotencyKey("pedido-A-1042"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(invoice.ID)
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.RequestOptions;
import com.fiscalisto.model.InvoiceCreate;
import com.fiscalisto.model.InvoiceCreateItem;
import java.util.List;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var invoice = fiscalisto.invoices().create(new InvoiceCreate()
        .setCustomer("cus_OufpKqnrvx9p1")
        .setPaymentForm("04")
        .setItems(List.of(
                new InvoiceCreateItem()
                        .setProduct("prd_OufpKqnrvx9p1")
        )), RequestOptions.idempotencyKey("pedido-A-1042"));
System.out.println(invoice.getId());
```

```csharp tab="C#"
using Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var invoice = await fiscalisto.Invoices.CreateAsync(new InvoiceCreate
{
    Customer = "cus_OufpKqnrvx9p1",
    PaymentForm = "04",
    Items = new() { new() { Product = "prd_OufpKqnrvx9p1" } },
}, new RequestOptions { IdempotencyKey = "pedido-A-1042" });
```

## Dónde se usa

Todo `POST` de la API acepta `Idempotency-Key`, como en Stripe o Resend:

- **Lo que timbra la exige**: facturas, sustituciones, complementos de pago, notas de crédito, anticipos y su aplicación, recibos de nómina, constancias de retenciones, lotes y facturar una cotización. Sin ella responden `idempotency_key_missing`.
- **Lo demás la acepta**: crear un cliente, un producto, un webhook, una cotización, una factura recurrente, un ticket, una descarga del SAT, mandar un correo, conectar una tienda… Con la llave, un reintento contesta lo mismo que la primera vez, con `idempotent-replayed: true`, y no crea nada otra vez ni manda otro correo.

`GET` y `DELETE` no la necesitan: repetirlos no cambia nada.

## Cómo elegirla

- De 1 a 255 caracteres ASCII, sin acentos ni ñ (8 o más en lo que timbra). Si no es así, `400` `idempotency_key_invalid`.
- Una por comprobante, tomada de algo que su sistema ya tiene y que no cambia al reintentar: el id del pedido, del cobro o de la fila en su base de datos (`pedido-A-1042`, `pago-7781`). Si no tiene uno, genere un UUID al crear el pedido y guárdelo con él.
- Nunca la genere en el momento de llamar: un reintento tendría otra llave y timbraría dos veces.

## Qué contesta al repetirla

| Situación | Respuesta |
| --- | --- |
| La primera vez | `201` con el comprobante timbrado (o `202` si el PAC aún no contesta). |
| Repetida, ya timbrado | `201` (lo mismo que la primera vez) con el mismo comprobante y el encabezado `idempotent-replayed: true`. No usa otro timbre. |
| Repetida mientras la primera sigue timbrando | `409` `request_in_progress` con `Retry-After`. Espere y repítala. |
| Repetida con otro cuerpo | `409` `idempotency_key_reused`. No se timbra nada. |
| La primera no pasó la revisión (`422` `invoice_invalid`) | La llave queda libre: mande el cuerpo corregido con la misma llave. |
| La primera la rechazó el SAT (`422` `invoice_rejected`) o no había timbres (`402`) | Repetirla con el mismo cuerpo lo intenta de nuevo. Con otro cuerpo, `409` `idempotency_key_reused`. |

La llave guarda el cuerpo con el que se usó desde que llegó al PAC. Si el SAT rechazó una factura y la corrige, el cuerpo es otro: use una llave nueva (`pedido-A-1042-2`). Repetir con la misma llave y el mismo cuerpo sirve cuando el problema no estaba en los datos: el PAC no respondió, se acabaron los timbres y ya compró, o el emisor aún no tenía su CSD. Un `invoice_invalid` nunca llegó al PAC, así que ahí sí puede corregir y repetir con la misma llave.

Varias solicitudes con la misma llave al mismo tiempo (un doble clic, dos servidores) timbran una sola vez: una recibe el comprobante y las demás `409` `request_in_progress` hasta que termine; repetidas, reciben el mismo comprobante.

## Cuánto dura una llave

Una llave es de su cuenta y de su modo: la misma en pruebas y en producción, o en dos [entornos de prueba](https://fiscalisto.com/docs/api/pruebas-y-produccion#un-entorno-de-prueba-para-cada-persona), son llaves distintas.

- En todo `POST` la llave queda ligada a esa ruta y ese cuerpo 24 horas: usarla para otra cosa responde `409` `idempotency_key_reused`, aunque sea otra ruta.
- Solo se guardan las respuestas correctas (`2xx`). Si la primera vez respondió un error, no se hizo nada y la llave queda libre: corrija y repita con la misma llave.
- Lo que timbra guarda además el comprobante de cada llave para siempre: una llave que ya timbró siempre contesta ese comprobante, aunque pasen meses.

Después de 24 horas, la llave de un `POST` que no timbra vuelve a estar libre.

## Reintentos recomendados

Los SDKs ([TypeScript, Python, PHP, .NET, Go y Java](https://fiscalisto.com/docs/api/sdk)) ya hacen todo esto en cada `POST`: ponen una `Idempotency-Key` si usted no la da, la conservan en cada reintento y esperan lo que diga `Retry-After`. Así ningún reintento del SDK crea o manda algo dos veces. Lo siguiente es para llamar al API directo.

Reintente con la misma llave cuando:

- no llegó respuesta (se cortó la conexión o se acabó el tiempo de espera);
- llegó `409` `request_in_progress`, `429` o un `5xx`, esperando lo que diga `Retry-After` o, si no viene, con espera creciente (1, 2, 4, 8 segundos).

No reintente un `400`, `402`, `403`, `404` o `422` sin cambiar algo: la respuesta sería la misma.

```js title="reintentar.js"
async function timbrar(body, idempotencyKey) {
  for (let intento = 0; intento < 5; intento++) {
    let res;
    try {
      res = await fetch("https://api.fiscalisto.com/v1/invoices", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.FISCALISTO_API_KEY}`,
          "Idempotency-Key": idempotencyKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
    } catch {
      // Sin respuesta: repetir con la misma llave nunca timbra dos veces.
    }
    if (res && res.status !== 409 && res.status !== 429 && res.status < 500) return res.json();
    const espera = Number(res?.headers.get("retry-after")) || 2 ** intento;
    await new Promise((r) => setTimeout(r, espera * 1000));
  }
  throw new Error("Fiscalisto no respondió; reintente más tarde con la misma Idempotency-Key.");
}
```

> **409 idempotency_key_reused no se reintenta.** El reintento del ejemplo vuelve a mandar el mismo cuerpo, así que un `409` es siempre `request_in_progress`. Si su código puede cambiar el cuerpo entre intentos, revise `error.code` antes de esperar.
