# Reintentos

Qué repetir, qué hacer ante un timeout y qué no concluir de un 401.

Repite con la misma clave cuando el error lo permita, y consulta antes de concluir. El campo `retryable` de cada respuesta te lo dice, y manda sobre el código: un rechazo ya registrado llega con `false` aunque ese código admita reintentos en otros casos. La [página de cada operación](/docs/operaciones/) lista sus códigos. Esta tabla usa [`POST /v1/payments`](/docs/operaciones/crear-pago/) como ejemplo.

| Lo que recibes | Qué hacer |
|---|---|
| Timeout o conexión cortada | Repite con la misma clave |
| `202 processing` | No repitas. Consulta el `pay_` con [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) |
| `freshness.status` en `delayed` | No repitas ni concluyas nada. Consulta más tarde |
| `409 idempotency_in_progress` | Espera `Retry-After`, normalmente 2 segundos, y repite con la misma clave |
| `409 payment_in_flight` | Repite con la misma clave cuando se resuelva |
| `429 rate_limited` | Espera `Retry-After` |
| `500`, `503` | Espera exponencial, misma clave |
| `422 in_review` con `retryable: true` | Espera la revisión. Misma clave |
| `422 in_review` con `retryable: false` | Quedó registrado. Espera la revisión y usa una clave nueva |
| Estado `in_review` en un pago | No repitas: el pago ya existe. Consulta el `pay_` |
| `422 insufficient_funds` | Fondea y repite con una clave nueva |
| `401`, `403` de acceso | Corrige el acceso. **No generes una clave nueva** |
| `failed` | El pago no se hizo. Para intentarlo de nuevo, usa una clave nueva |

## Un 401 o 403 no dice si un pago ocurrió

Un error de acceso sólo dice que esta petición no pasó. Una anterior pudo haber pasado. Nunca generes una clave nueva por eso: consulta el `pay_`, o repite con la misma clave cuando recuperes el acceso.

## Espera exponencial

Empieza en un segundo, duplica en cada intento, agrega variación al azar y pon un tope. Respeta `Retry-After` si viene.

**Variables de estas muestras: simulador público, sin cuenta**

Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público:

```bash
export FITAL_API="https://widget.fital.xyz/explorar/v1"
export FITAL_KEY="sk_test_sandbox_publica"
export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico"
```

En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/).

```bash
CUERPO='{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}'
CLAVE="pago-factura-1042"
TS=$(date +%s)
HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1)
FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \
  | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1)
curl -sS -X POST "$FITAL_API/payments" \
  -H "Authorization: Bearer $FITAL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $CLAVE" \
  -H "X-Fital-Timestamp: $TS" \
  -H "X-Fital-Signature: v1=$FIRMA" \
  -d "$CUERPO"
```

```js
import { createHash, createHmac } from 'node:crypto';
const cuerpo = JSON.stringify({"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"});
const clave = 'pago-factura-1042';
const ts = String(Math.floor(Date.now() / 1000));
const hash = createHash('sha256').update(cuerpo).digest('hex');
const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET)
  .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex');
const r = await fetch(`${process.env.FITAL_API}/payments`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FITAL_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': clave,
    'X-Fital-Timestamp': ts,
    'X-Fital-Signature': `v1=${firma}`,
  },
  body: cuerpo,
});
console.log(r.status, await r.json());
```

```python
import hashlib, hmac, json, os, time, urllib.error, urllib.request
datos = {"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}
cuerpo = json.dumps(datos).encode()
clave = "pago-factura-1042"
ts = str(int(time.time()))
canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave])
firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest()
pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo,
    headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"],
             "Content-Type": "application/json",
             "Idempotency-Key": clave,
             "X-Fital-Timestamp": ts,
             "X-Fital-Signature": "v1=" + firma})
try:
    with urllib.request.urlopen(pet) as r:
        print(r.status, json.load(r))
except urllib.error.HTTPError as e:
    print(e.code, e.read().decode())
```

```php
<?php
$cuerpo = '{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}';
$clave = 'pago-factura-1042';
$ts = (string) time();
$canon = implode("\n", [$ts, 'POST', '/v1/payments', hash('sha256', $cuerpo), $clave]);
$firma = hash_hmac('sha256', $canon, getenv('FITAL_SIGNING_SECRET'));
$c = curl_init(getenv('FITAL_API') . '/payments');
curl_setopt_array($c, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('FITAL_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: ' . $clave,
    'X-Fital-Timestamp: ' . $ts,
    'X-Fital-Signature: v1=' . $firma,
  ],
  CURLOPT_POSTFIELDS => $cuerpo,
]);
echo curl_exec($c), "\n";
```

## Errores comunes

- Marcar como fallido un pago por un timeout.
- Reintentar sin tope.
- Decidir por el código sin mirar `retryable`: `in_review` puede llegar con los dos valores.

## Qué sigue

[Límites](/docs/limites/).
